Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
75 changes: 75 additions & 0 deletions .claude/rules/api-mobile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
paths: ["hogwarts/features/**/services/*.swift", "hogwarts/core/network/*.swift"]
description: /api/mobile/* contract enforcement for all backend interactions
---

# /api/mobile/* Contract Rule

When implementing service layer (`services/<feature>-actions.swift`):

## Endpoint base

- ✅ All endpoints use `/api/mobile/*` prefix
- ✅ Production: `https://kingfahad.databayt.org/api/mobile/`
- ✅ Demo: `https://demo.databayt.org/api/mobile/`
- ❌ NEVER call non-mobile endpoints (cookie auth, NextAuth session, etc.)

## Request shape

- ✅ `Authorization: Bearer <jwt>` header
- ✅ `Content-Type: application/json`
- ✅ JSON request body with snake_case keys
- ✅ Optional `X-School-Id: <schoolId>` header (server reads JWT claim primarily)

## Response shape

- ✅ Snake_case JSON
- ✅ Always paginated lists with `{ data: [...], meta: { page, pageSize, total } }`
- ✅ Always include `school_id` on entity payloads (verify matches `TenantContext`)
- ✅ Errors: `{ error: { code, message, details? } }`

## Decoding

- ✅ `JSONDecoder.keyDecodingStrategy = .convertFromSnakeCase` is set globally on `APIClient`
- ✅ Date decoding: `JSONDecoder.dateDecodingStrategy = .iso8601`
- ✅ Use `Decodable` structs in `models/` mirroring backend DTOs

## Auth refresh

- ✅ On 401, attempt one refresh via `PUT /api/mobile/auth` with `X-Refresh-Token: <refresh>`
- ✅ On second 401, log user out
- ✅ Token refresh is race-safe (single in-flight refresh, queued requests retry)

## Service file convention

```swift
// hogwarts/features/<feature>/services/<feature>-actions.swift
@MainActor
final class FeatureActions {
private let api: APIClientProtocol

init(api: APIClientProtocol = APIClient.shared) {
self.api = api
}

func list() async throws -> [FeatureItem] {
try await api.get("/mobile/<feature>", as: PagedResponse<FeatureItem>.self).data
}

func get(id: String) async throws -> FeatureItem {
try await api.get("/mobile/<feature>/\(id)", as: FeatureItem.self)
}
}
```

## Mocking for tests

- ✅ Tests inject `MockAPIClient` (in `HogwartsTests/sync-engine-mock-tests.swift`)
- ✅ Fixtures stored in `HogwartsTests/fixtures/<feature>/*.json`
- ❌ NEVER hit live API in unit tests

## Reference

- Contract source-of-truth: `/Users/abdout/hogwarts/src/app/api/mobile/README.md`
- iOS migration guide: same README, "iOS Swift App" section
- Backend gaps tracker: `docs/backend-gaps.md`
48 changes: 48 additions & 0 deletions .claude/rules/i18n.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
paths: ["hogwarts/**/*.swift"]
description: i18n + RTL + content-language enforcement on every Swift view
---

# i18n / RTL / Content-Language Rule

When editing Swift files in `hogwarts/`:

## Forbidden

- ❌ Hardcoded user-visible strings (`Text("Submit")`, `.alert("Error")`, etc.)
- ❌ `.left` / `.right` modifiers — use `.leading` / `.trailing`
- ❌ `HorizontalAlignment.left/.right` — use `.leading/.trailing`
- ❌ Literal date format strings (`"dd/MM/yyyy"`) — use `Date.FormatStyle`
- ❌ `Locale.current.currency` for school amounts — use `TenantContext.shared.currency`
- ❌ Assuming entity content lang matches device lang

## Required

- ✅ Use `Text("namespace.key")` or `String(localized: "namespace.key")`
- ✅ Add EN + AR string pairs simultaneously to `Localizable.xcstrings`
- ✅ Use `.leading` / `.trailing` everywhere (HStack handles layout direction automatically)
- ✅ For directional SF Symbols, use `.environment(\.layoutDirection, ...)` or check `flipsForRightToLeftLayoutDirection`
- ✅ Render entity content with `entity.lang` font + direction (override `\.layoutDirection` per-card if needed)
- ✅ Show "Translate" affordance when `entity.lang != app.currentLanguage`
- ✅ Provide localized `.accessibilityLabel(...)` on every meaningful element

## String key convention

Format: `<namespace>.<screen>.<element>` (snake_case allowed)

Examples:
- `auth.login.title`
- `attendance.history.empty_state`
- `messaging.compose.placeholder`

## Verification per change

Before merging:
1. Run `bash scripts/audit-i18n-hardcoded.sh` — must pass
2. Run `bash scripts/check-string-parity.sh` — must pass
3. Take screenshots in `ar` and `en`, attach to PR
4. Toggle in-app language during runtime — UI flips without restart

## Reference

See `docs/i18n.md` for the full playbook.
42 changes: 42 additions & 0 deletions .claude/rules/multitenant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
paths: ["hogwarts/**/*.swift"]
description: schoolId scoping invariants for queries, caches, and mutations
---

# Multi-Tenancy Rule

When writing or editing data layer code (services, view-models, models, caches):

## Required on every data path

- ✅ Every `@Model` declares `var schoolId: String`
- ✅ Every `FetchDescriptor<T>` includes `#Predicate { $0.schoolId == schoolId }`
- ✅ Every ViewModel reads `TenantContext.shared.currentSchoolId`, never view-arg
- ✅ Every cache key prefixed with `<schoolId>:`
- ✅ Every mutation logs `AuditEvent(tenantId: schoolId, ...)` to `core/audit/audit-log.swift`
- ✅ Verify response payload `school_id` matches `TenantContext` (defense in depth)

## Forbidden

- ❌ `FetchDescriptor` without `schoolId` predicate
- ❌ Hardcoded `schoolId: "..."` in queries (must come from TenantContext)
- ❌ Storing tokens or user data in `UserDefaults` — use Keychain via `core/auth/keychain-service.swift`
- ❌ Cross-tenant lookups (joining tables without schoolId)

## When the user switches schools

- Invalidate image caches, document caches, search index
- Clear in-memory state of view-models
- Re-fetch profile and permissions
- Reset SyncEngine

## Verification

Before merging:
1. Run `bash scripts/audit-tenant-scope.sh` — must pass
2. Add multi-tenant isolation test in `HogwartsTests/<feature>/<feature>-tenant-isolation-tests.swift`
3. Test scenario: create record in school A, switch to school B, verify NOT visible

## Reference

See `docs/multitenancy.md` for the full invariants checklist.
33 changes: 33 additions & 0 deletions .claude/rules/roles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
paths: ["hogwarts/features/**/*.swift", "hogwarts/app/**/*.swift"]
description: RBAC enforcement on every feature surface
---

# Role-Based Access Rule

When editing feature views, ViewModels, or navigation:

## Required

- ✅ Every screen guards entry by role: `guard TenantContext.shared.currentRole?.can(.<permission>) == true else { ... }`
- ✅ Every action button / menu item that requires a permission is hidden for non-permitted roles, not just disabled
- ✅ The route table in `home-tile-spec.swift` declares `roles: [UserRole]` per tile
- ✅ Server-side 403 is gracefully handled (show role-mismatch error, suggest switching role/school)

## Permission catalog

`hogwarts/core/auth/authorization.swift` is the single source of truth. Add permissions there as a `Permission` enum case before using.

## Multi-role users

- One role active at a time (current session)
- Switch via Profile → Switch Role → re-fetch profile + permissions
- Some users hold roles in multiple schools — handle as separate sessions

## Frontmatter declaration

Every story declares `roles: [<roles>]` in frontmatter listing all roles that see this surface.

## Reference

See `docs/roles.md` for the full role-feature matrix.
59 changes: 59 additions & 0 deletions .github/workflows/i18n-and-tenant-gates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: i18n + Multi-Tenant Gates

on:
pull_request:
paths:
- 'hogwarts/**/*.swift'
- 'hogwarts/resources/Localizable.xcstrings'
- 'scripts/check-string-parity.sh'
- 'scripts/audit-tenant-scope.sh'
- 'scripts/audit-i18n-hardcoded.sh'
push:
branches: [main]

jobs:
string-parity:
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: Install jq
run: brew install jq

- name: Check EN/AR string parity (≥99%)
run: |
chmod +x scripts/check-string-parity.sh
THRESHOLD=0.99 scripts/check-string-parity.sh

hardcoded-strings:
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: Audit hardcoded UI strings
run: |
chmod +x scripts/audit-i18n-hardcoded.sh
scripts/audit-i18n-hardcoded.sh

tenant-scope:
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: Audit FetchDescriptor schoolId scoping
run: |
chmod +x scripts/audit-tenant-scope.sh
scripts/audit-tenant-scope.sh

rtl-snapshot-check:
runs-on: macos-15
if: contains(github.event.pull_request.changed_files, 'hogwarts/features')
steps:
- uses: actions/checkout@v4

- name: Verify RTL snapshots exist for changed views
run: |
# Heuristic: every modified feature/views/*-view.swift must have at
# least one snapshot in HogwartsTests/snapshots/<feature>/ in both ar and en
# (skip if no snapshot infrastructure yet — warn only)
echo "RTL snapshot check (warn-only until snapshot infrastructure lands)"
27 changes: 27 additions & 0 deletions ExportOptions.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>destination</key>
<string>export</string>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>org.databayt.Hogwarts</key>
<string>Hogwarts App Store</string>
</dict>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>YOUR_TEAM_ID</string>
<key>uploadSymbols</key>
<true/>
<key>uploadBitcode</key>
<false/>
<key>compileBitcode</key>
<false/>
</dict>
</plist>
Loading
Loading