Repository navigation
Spec 10 data and api
← 09. Streaming media: radio, webcams, schedules, movie · Spec index
The app reads almost everything over HTTPS. Most requests go to ccc-server, a Koa proxy and cache. St. Olaf's deployment is at https://stolaf.frogpond.tech/v1/; developers can override that address. Carleton's deployment is at https://carleton.frogpond.tech/v1/ and cannot be overridden. A published "sources manifest" (a JRD document from GET sources, with a copy bundled in the build) tells the app where feature data lives. Each source is either relative, meaning a ccc-server proxy, or absolute, meaning the app calls the third party directly. React Query caches responses in memory with a 24-hour gcTime. Successful queries are saved to AsyncStorage as one JSON blob, and a blob written by any other build is thrown away. Online status comes from NetInfo, and the app counts as focused while it is in the foreground. Some data does not live in the query cache. Calendar events and the course catalog live in SQLite (aao.db plus an attached course-catalog.db in Caches). Settings live in redux-persist (persist:root), eleven Zustand persisted stores, two prefixed AsyncStorage keys, an expo-sqlite key-value store (telemetry consent), the Keychain (SIS credentials) and UserDefaults (DebugSwift switch). "Reset Everything" and the UI-test reset wipe most of this; the exceptions are listed under Persisted state. This chapter documents the shared machinery and the full on-device inventory. It also catalogues every ccc-server endpoint for both institutions, with its upstream, server cache TTL and HTTP caching behaviour, and marks which ones the app calls.
App (/home/claude/AAO-React-Native):
-
modules/api/index.ts: the kyclientandcarletonClientand the API root getter and setters. -
source/init/api.ts: sets the API roots at launch. -
source/lib/constants.ts:DEFAULT_URL,CARLETON_DEFAULT_URL. -
source/lib/api-url.ts:apiUrl()resolves an href against the API root. -
source/lib/remote-images.ts: ccc-server image URLs and prefetch. -
source/lib/probe.ts: CI log probe. -
source/lib/refresh.ts:refreshApp, the Reset Everything action. -
source/lib/clear-stored-data.ts. -
source/lib/uitest-reset.ts. -
source/lib/storage.ts,modules/storage/index.ts: prefixed AsyncStorage keys. -
source/lib/login.ts: Keychain credentials and the OleCard login. -
source/lib/layout-store.ts. -
source/lib/infinite-data.ts. -
source/init/tanstack-query.ts: QueryClient, persister, onlineManager, focusManager. -
app/_layout.tsx: provider order,watchQueryFailures. -
modules/data-sources/*: manifest,fetchSourceBody,parseEach,bundled.json. -
data/sources.yaml: the published manifest source. -
source/database/client.ts,schema.ts,sql.ts,calendar/schema.ts,calendar/write.ts(retention),calendar/read.ts(keys),calendar/revision.ts,courses/catalog-file.ts,courses/refresh.ts,courses/read.ts,courses/schema.ts. -
source/redux/store.ts,migrations.ts,sentry-enhancer.ts,parts/settings.ts,parts/buildings.ts,parts/courses.ts. - Zustand persisted stores:
source/features/telemetry/store.tssource/features/streaming/radio/store.tssource/features/mess/store.tssource/features/developer/store.tssource/features/athletics/store.tssource/features/sis/student-work/store.tssource/features/quick-actions/store.tssource/features/news/store.tssource/features/map/store.tssource/features/faqs/store.tssource/features/calendar/store.tssource/features/student-orgs/store.ts
-
source/features/telemetry/query-failures.ts,catalog.ts,track.ts;source/init/sentry.ts,sentry-options.ts,scrub.ts: network-related reporting only. -
modules/notice/notice.tsx,modules/notice/list-state.ts,source/features/menus/lib/menu-view.ts: shared offline, error and loading states. -
source/features/developer/server-url.tsx,query.ts,use-server-discovery.ts,developer.tsx,api-test/query.ts;app/developer/api-test/*.tsx,app/developer/bon-app-picker.tsx. -
plugins/with-app-delegate-customizations.ts:--reset-stateand URLCache. -
app.config.ts:227-239: ATS and Bonjour. -
source/init/chaos.ts,source/chaos/*: fault injection, test only. - Every
query.ts/*-query.tsthat issues a request, catalogued under Data.
Server (/home/claude/ccc-server):
-
ccc-server:source/ccc-server/app.ts,server.ts,index.ts,helpers.ts,response-cache.ts. -
ccc-server:source/ccc-koa/cache.ts,conditional-get.ts,ctx-cache-control.ts,behind-proxy.ts,client-abort.ts. -
ccc-server:source/ccc-lib/http.ts,images.ts,stored-headers.ts,constants.ts,deprecated.ts. -
ccc-server:source/ccci-stolaf-college/index.tsand everyv1/*.ts. -
ccc-server:source/ccci-carleton-college/index.tsand everyv1/*.ts. -
ccc-server:source/feeds/*,calendar/deprecated.ts,student-orgs/presence.ts,student-work/oracle.ts,menus-bonapp/index.ts,athletics/index.ts(shapes only).
This area owns no end-user screen. It owns the shared states that every data screen draws and the developer screens that change networking.
-
Loading:
LoadingView(modules/notice/loading.tsx). -
Error with nothing saved:
LoadErrorView. TitleCouldn’t Loadby default, the SF Symbolexclamationmark.triangle, and a description fromdescribeError(error). That description is the error'smessage, orSomething went wrong.when there is none. A bordered, largeTry Againbutton triggers the retry (modules/notice/notice.tsx:70-99). The user therefore sees raw messages such asNews fetch failed: 500(aSourceFetchError) or ky's HTTPError text. -
Offline with nothing saved: decided by
listState()(modules/notice/list-state.ts:12-26), checked in order:- Any saved data gives
list; data always wins. - Otherwise an error gives
error. - Otherwise a query that is paused while NetInfo says offline gives
offline. - Otherwise a pending query gives
loading. - Anything else gives
list.
A pause while NetInfo reports online stays
loading, because React Query also pauses for an inactive app. Screens drawofflineas aNoticeViewwith titleOffline, symbolwifi.slashand a feature-specific description. Known copy:- Menus:
You're offline, and this menu hasn't been saved on this phone yet. It will load once you're back online.(source/features/menus/lib/menu-view.ts:48-49). Menus use their ownmenuView, which reportsofflineonisPausedalone, without the NetInfo check. - Student Work:
Student Work needs a connection to load the job board the first time., with aTry Againaction (app/student-work/index.tsx:23,84-93). - Course search:
Course search needs a connection to download the course catalog the first time.(source/features/sis/course-search/lib/list-state.ts:5-6).
- Any saved data gives
-
There is no app-wide offline banner. NetInfo is used only to drive React Query's
onlineManager(source/init/tanstack-query.ts:155-159). Messenger screens showPageLoading pausedwhenfetchStatus === 'paused'; see the Messenger chapter.
Developer → Server URL section (source/features/developer/server-url.tsx)
-
How it is reached: the Developer screen (
app/developer/index.tsx, titleDeveloper, identifierscreen-developer). It opens from Home's Developer tile in dev mode, which is a debug build or the persisteddevModeOverride. -
Layout: a form section titled
Server URLwith footerEmpty means we will use the default URL.- While the stored value loads, the text field is disabled and shows placeholder
Loading…. - Once loaded, the text field shows the stored address, with placeholder
https://stolaf.frogpond.tech/v1/and keyboard submit labeldone. Under it is an action row titledSave. That row readsInvalid URL!and is disabled when the text is non-empty and not anhttp:/https:URL. - The field and button are disabled while saving.
- While the stored value loads, the text field is disabled and shows placeholder
-
Saving: submitting the field or pressing Save stores the address, appending a trailing
/if it is missing (source/lib/storage.ts:57). The JS app then restarts (Restart()). An empty value is stored as'', which means the default is used. -
Local Servers section (
Local Servers, footerTap a server to use it.): shown only in dev mode, only when the RNZeroconf native module is linked, and only once at least one service has been found. The app browses Bonjour type_ccc-server._tcpinlocal.. Each found service becomes the URLhttp://<first address or host without trailing dot>:<port><txt.path or '/v1/'>; an IPv6 address is bracketed. Tapping a row fills the field without saving. A service that goes away removes its row (use-server-discovery.ts:19-67).
Developer → Reset Everything (source/features/developer/developer.tsx:12-21,57)
- An action row
Reset Everythingopens an alert titledReset Everything, with messageAre you sure you want to clear everything?and buttonsNope!(cancel) andReset it!(destructive). - Confirming runs
refreshApp(); see Behaviors.
Developer → API Tester (app/developer/api-test/index.tsx, detail.tsx)
-
List: title
API Tester. It listsGET routesgrouped by first path segment, with pull-to-refresh, and has a bottom search bar with placeholder/path/to/uri. -
States: loading,
LoadErrorView, orNo Routes. -
Detail: tapping a route, or searching, opens the detail screen. It fetches
client.get(<displayName trimmed and lowercased>)as text, withstaleTime: 0andgcTime: 0, and shows it raw or as syntax-highlighted JSON.
Developer → Bon Appetit Picker (app/developer/bon-app-picker.tsx)
- Title
Dev BonApp Picker, with a numericidfield. - With nothing entered it shows
No Café ID/Enter a café’s ID to see its menu.(symbolfork.knife). - An entered id loads the BonApp menu for
{id}, which usesfood/menu/:id?cafeId=:idandfood/cafe/:id?cafeId=:id.
-
St. Olaf base:
DEFAULT_URL = 'https://stolaf.frogpond.tech/v1/'(source/lib/constants.ts:3).- At launch,
source/init/api.ts:11-21readsaao::settings:server-addressand uses it if it is non-empty, else the default. It then createsclient = ky.create({baseUrl})(modules/api/index.ts:16-19). - The read is async. Until it finishes,
clientis undefined, so any request throughclientbefore then would throw.apiUrl()andimageUrl()fall back toDEFAULT_URLwhile the root is unset (source/lib/api-url.ts:13-15,remote-images.ts:30-33).
- At launch,
-
Carleton base:
CARLETON_DEFAULT_URL = 'https://carleton.frogpond.tech/v1/'(constants.ts:6). It is set synchronously before the St. Olaf read and is never overridable (source/init/api.ts:8). It is used only forspaces/hoursandmap/geojson(source/features/building-hours/query.ts:31-33,source/features/map/query.ts:22-24). -
Relative paths: always without a leading slash, resolved against a base that ends in
/. -
Headers:
- The app sets no User-Agent, no Accept and no auth headers on
client(default iOS/React Native fetch UA, inferred). - Sentry adds
sentry-trace/baggageto requests whose URL matcheslocalhost,frogpond.techor^/(source/init/sentry.ts:41) when reporting is enabled. - The course catalog request adds
If-None-Match(source/database/courses/refresh.ts:84-86). - PaperCut requests send
Content-Type: application/x-www-form-urlencodedandOrigin: https://papercut.stolaf.edu(source/lib/stoprint/urls.ts:9-15).
- The app sets no User-Agent, no Accept and no auth headers on
-
Timeouts:
- Requests through
clientuse ky's default 10 s per attempt (modules/data-sources/fetch-source.ts:80-86, comment). - Absolute-href sources fetched by
fetchSourceBodyget a manual 10 000 ms abort timer that is also tied to the caller's AbortSignal (fetch-source.ts:3,26-41). - Plain
ky.getcalls (directory search, OleCard, PaperCut) use ky's defaults (inferred: 10 s timeout and ky's retry policy).
- Requests through
-
Retries:
- React Query defaults apply: 3 retries with exponential backoff,
networkMode: 'online'(library default, inferred,@tanstack/react-query5.104.0). - ky's own retry runs beneath React Query for
clientandkycalls (inferred ky 2.1.0 default: 2 retries for GET on 408/413/429/500/502/503/504). - Overrides:
- manifest:
retry: false,networkMode: 'offlineFirst' - org detail:
retry: false - course catalog:
shouldRetryCatalog, at most 3 and never for a rejected file - map categories: no retry for
UnreadableMapCategoriesError, else at most 3 - Messenger story: no retry on a missing story
- Messenger playlist page:
retry: 1
- manifest:
- React Query defaults apply: 3 retries with exponential backoff,
-
Error mapping:
-
fetchSourceBodyturns any error status, for a relative or an absolute href, intoSourceFetchError(message = "<label> fetch failed: <status>", status, code).codeis a WordPress error body'scode, read only when the content type containsjson(fetch-source.ts:47-124). - Other errors (ky
HTTPError,TimeoutError, networkTypeError) pass through unchanged.
-
-
parseEach(modules/data-sources/parse-each.ts): items that fail their schema, or whose conversion throws, are dropped. A non-empty response in which every item failed throwsevery <what> was malformed. An empty response stays empty.
React Query defaults and persistence (source/init/tanstack-query.ts)
-
QueryClient:
gcTimeis 24 h for all queries (:20-28). Other defaults are the library's:staleTime0, refetch on mount, focus and reconnect when stale (inferred). -
Focus: an AppState change to
activemarks the app focused, and any other state unfocused (:162-166). Stale active queries therefore refetch when the app returns to the foreground (inferred library behaviour). -
Online: a NetInfo listener sets online to
Boolean(state.isConnected)(:155-159). Reachability (isInternetReachable) is ignored. Reconnecting resumes paused fetches and refetches stale queries (inferred). -
Persister:
createAsyncStoragePersister({storage: AsyncStorage, serialize: serializeCache})(:90-93). It writes the whole cache as one AsyncStorage value under the library default keyREACT_QUERY_OFFLINE_CACHE, throttled to 1 s (library defaults, inferred). -
Restore:
PersistQueryClientProviderinapp/_layout.tsx:112, inside redux'sPersistGate, which showsLoading App...until redux rehydrates (:111). NomaxAgeis given, so the library default of 24 h applies (inferred): a saved cache older than 24 h is discarded on restore. -
Buster:
`${nativeApplicationVersion}+${nativeBuildVersion}`(:121). A cache saved by any other build, or with no buster, is dropped (source/init/__tests__/tanstack-query.test.ts:204-212). -
What is dehydrated (
shouldDehydrateQuery,:139-146), checked in this order:- A key whose first element is a string starting with
calendar(isCalendarQueryKey,:110-113) is never persisted. This covers['calendar','named',…]andcalendar-db. -
meta.persist === falseis never persisted. -
meta.persistPagesset, on infinite data, is persisted. -
meta.persistAfterFailureset, with data present, is persisted. - Otherwise the query is persisted only when
status === 'success'.
- A key whose first element is a string starting with
-
Serialization (
serializeCache,:75-88):- An infinite query with
persistPages: Nis cut to its first N pages and written as success, with no error. - A
persistAfterFailurequery in error with data is written as success with its old data.
- An infinite query with
-
Queries opted out (
meta.persist: false):- course catalog receipt and every
course-dbread (source/database/courses/read.ts:48,115,163,199) - radio now-playing (
source/features/streaming/radio/use-now-playing.ts:43) - Messenger lead text (
source/features/mess/query.ts:153) - Messenger issues not opened with
persist: true
- course catalog receipt and every
-
Queries with
persistPages: 1: Messenger feed, category lists and issues list (mess/query.ts:110,172,188). -
persistAfterFailure: the sources manifest (modules/data-sources/resolve.ts:32). -
Pull to refresh on infinite lists:
refetchFromFirstPagetrims each active infinite query under the key to page 1, then refetches the active queries (source/lib/infinite-data.ts:28-34).
-
Request:
GET {base}sourcesthroughclient, parsed withJrdSchema. -
Schema:
{subject: string, links: [{rel: URL, href: non-empty URI reference without whitespace or <>"{}|\^\, type: non-empty, titles?: record, properties: {"https://frogpond.tech/ns/id": non-empty}}]}(types.ts:30-45`). -
Query: key
['data-sources','manifest'],staleTime24 h,networkMode: 'offlineFirst',retry: false(resolve.ts:16-37). -
Resolution rules:
-
fetchManifestreturns the bundled copy when the fetch fails or the response is malformed, so it never hangs offline (resolve.ts:41-47). - A missing source falls back to its bundled entry.
- A source whose type is unsupported falls back to its bundled entry. If the bundled entry is also unsupported, resolution throws
no supported source for rel "<rel>" and id "<id>"(resolve.ts:79-97). -
resolveSourceslists every id from the fetched and bundled documents and drops any id that cannot resolve (resolve.ts:99-116).
-
-
useManifest(radio) returnsundefinedwhile the persisted cache is restoring. After that it returns the cached manifest however old, or{subject:'', links:[]}, which resolves to the bundled entries (use-manifest.ts:18-25). -
Bundled and published entries (
modules/data-sources/bundled.json,data/sources.yaml); "relative" means served by ccc-server:
| rel (suffix) | id | type | href |
|---|---|---|---|
| news | stolaf | wordpress.v2.posts+json |
https://wp.stolaf.edu/wp-json/wp/v2/posts?per_page=10&_embed=true (direct) |
| news | mess | wordpress.v2.posts+json |
https://olafmessenger.com/wp-json/wp/v2/posts?per_page=50&_embed=true (direct) |
| news | oleville | frogpond.feed-items+json |
news/named/oleville (relative; no UI uses it) |
| a-to-z | stolaf | stolaf.a-z+json |
https://wp.stolaf.edu/wp-json/site-data/sidebar/a-z (direct) |
| a-to-z | extras | frogpond.a-z-extras+json | a-to-z/extras |
| calendar | stolaf | tribe.events.v1+json |
https://wp.stolaf.edu/calendar/wp-json/tribe/events/v1/events?per_page=50 (direct) |
| calendar | presence | presence.events+json |
https://api.presence.io/stolaf/v1/events (direct) |
| calendar | krlx-schedule / ksto-schedule | frogpond.events+json |
calendar/named/krlx-schedule, calendar/named/ksto-schedule
|
| jobs | stolaf | oracle.recruiting-ce+json |
https://fa-ewur-saasfaprod1.fa.ocs.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1 (direct) |
| org-categories | stolaf | frogpond.org-categories+json | orgs/category-styles |
| map-categories | stolaf | frogpond.map-categories+json | map/categories |
| student-work-areas / student-wages / student-work-units | stolaf | frogpond.* |
student-work/areas, student-work/wages, student-work/units
|
| map-style | stolaf-light / stolaf-dark | maplibre.style+json |
map/style, map/style-dark
|
| course-catalog | stolaf | application/vnd.sqlite3 | courses/catalog.db |
| radio-stream | ksto / krlx | mpegurl / audio/mpeg |
https://cdn.stobcm.com/ksto/live.m3u8, https://s3.voscast.com:10803/stream (direct) |
| radio-player-page | ksto | text/html |
https://www.stolaf.edu/multimedia/play/embed/ksto.html (direct; only https://www.stolaf.edu is accepted) |
| radio-now-playing | krlx | metaradio.stationnow+json |
https://content.krlx.org/wp-json/metaradio/v1/stationnow/?station=1 (direct) |
All queries use React Query unless noted. "Persisted" follows the dehydration rules above.
| Feature | Query key | Request | staleTime / refetch | Notes |
|---|---|---|---|---|
| Manifest | ['data-sources','manifest'] |
GET sources |
24 h | persistAfterFailure |
| Student orgs | ['orgs'] |
GET orgs |
5 min | UI-test fixture |
| Org detail | ['org-detail', uri] |
GET orgs/uri/{encodeURIComponent(uri)}; 404 → null
|
5 min, no retry | query.ts:45-75 |
| Org memberships | ['org-category-memberships'] |
GET orgs/categories |
5 min | |
| Org category icons | ['org-category-icons'] |
manifest → orgs/category-styles; body .data
|
5 min | no source → []
|
| Webcams | ['streaming','webcams'] |
GET webcams → .data
|
default | |
| Streams | ['streams',{sort,dateFrom,dateTo}] |
GET streams/upcoming?sort=ascending&dateFrom=YYYY-MM-DD&dateTo=+2 months (campus time zone) |
default | |
| BonApp menu | ['cafe-menu','bonApp',path,day] |
GET food/named/menu/{stav-hall,the-cage,burton,ldc,sayles,weitz,schulze} or food/menu/{id}?cafeId={id}
|
1 h |
day = campus YYYY-MM-DD
|
| BonApp cafe | ['cafe-info','bonApp',path,day] |
GET food/named/cafe/{…} or food/cafe/{id}?cafeId={id}
|
1 h | |
| Pause menu | ['cafe-menu','hosted','food/named/menu/the-pause'] |
GET food/named/menu/the-pause → .data
|
default | |
| Athletics | ['athletics','scores'] |
GET athletics/scores |
refetchInterval 30 s if any game is in play, else 5 min | |
| Building hours | [campus,'buildings'] |
GET spaces/hours (Carleton client for carleton) → .data
|
default; 5 min from map cards | St. Olaf bundled under UI test or dev override |
| Map geojson | [campus,'map','geojson'] |
GET map/geojson (per-campus client) → .features
|
1 h; 5 min from cards | |
| Building directory | ['building-directory',campus] |
GET spaces/directory → .data (St. Olaf only) |
5 min | |
| Map categories | ['map-categories'] |
manifest → map/categories, validated with zod |
5 min | select re-validates the restored cache |
| Map style | ['map-style','stolaf',scheme] |
manifest only (no fetch of the style itself) | 24 h; initialData = shipped href | Carleton uses https://carls-app.github.io/map-tiles/style.json
|
| Transit modes | ['transit','modes'] |
GET transit/modes → .data
|
default | |
| Bus | ['transit','bus-routes'] |
GET transit/bus → .data
|
default | |
| Dictionary | ['dictionary'] |
GET dictionary → .data
|
5 min | |
| Contacts | ['contacts'] |
GET contacts → .data
|
5 min | |
| Departments | ['directory','departments'] |
GET directory/departments → .results, sorted by name.localeCompare
|
24 h | |
| Directory search | ['directory',{format:'json',<field>:q}] |
GET https://www.stolaf.edu/directory/search?format=json&{department|firstname|lastname|major|query|title|email}=<trimmed q> |
1 min | direct |
| FAQs | ['faqs'] |
GET faqs |
default | normalised, falls back to local FAQs |
| News | ['news','named',id] |
manifest → stolaf/mess WordPress (direct) | default | |
| A–Z | ['a-z'] |
manifest → wp.stolaf.edu A–Z (direct) plus a-to-z/extras (failures ignored) |
default | |
| Calendar ingest | ['calendar','named',id] |
manifest → TEC (direct, paged, today through +1 month) / Presence (direct) | 1 h | not persisted; writes SQLite |
| Radio schedules | ['schedule',id] |
GET calendar/named/{krlx,ksto}-schedule |
default | persisted |
| Now playing | ['radio-now-playing',station,url] |
KRLX metaradio (direct) | refetchInterval = feed refreshMs or 60 s, while playing |
persist false |
| Jobs board | ['jobs','postings'] |
Oracle HCM REST (direct): categories, then one requisitions call per category (limit 200, POSTING_DATES_DESC) |
5 min | |
| Job detail | ['jobs','detail',id] |
Oracle recruitingCEJobRequisitionDetails (direct) |
default | |
| Posting units | ['jobs','posting-units'] |
manifest → student-work/units
|
1 h | |
| Student Work areas / wages |
['student-work-areas'], ['student-wages']
|
manifest → student-work/areas, student-work/wages
|
5 min | zod-validated |
| Course catalog | ['catalog','course-catalog'] |
manifest → courses/catalog.db (307 → https://stolaf.dev/course-data/catalog-recent.db), If-None-Match
|
1 h | not persisted |
| Messenger | ['mess',…] |
https://olafmessenger.com/wp-json/wp/v2/* via the manifest's mess feed (direct); news/mess/wp/v2/* if the manifest points there |
5 min to 1 day | see Messenger chapter |
| Images | none (RN Image, cache: 'force-cache') |
GET images/{contacts,news-sources,spaces,streaming,webcams}/{name}.webp against the API root |
HTTP cache | prefetch errors ignored |
| Balances | ['balances',username] |
POST https://www.stolaf.edu/apps/olecard/checkbalance/authenticate.cfm (form username, password, credentials: include), then GET …/api.cfm
|
default | direct |
| Credentials | ['credentials','stolaf.edu'] |
Keychain read |
networkMode 'always', gcTime 0, staleTime 0 |
|
| StoPrint | ['printing',…] |
https://papercut.stolaf.edu/rpc/api/rest/internal/… (direct); color printers via GET printing/color-printers
|
default | |
| Dev routes | ['routes'] |
GET routes |
default | |
| Dev API test | ['api-test',name] |
GET {name} as text |
0 / gcTime 0 | |
| Server URL | ['settings','server-url'] |
AsyncStorage read | default |
sources-
orgs,orgs/uri/:uri,orgs/categories,orgs/category-styles webcamsstreams/upcomingfood/named/menu/{stav-hall,the-cage,the-pause,burton,ldc,sayles,weitz,schulze}food/named/cafe/{stav-hall,the-cage,burton,ldc,sayles,weitz,schulze}-
food/menu/:cafeId,food/cafe/:cafeId(dev picker only) athletics/scores-
spaces/hours(both institutions) spaces/directory-
map/geojson(both institutions),map/categories,map/style,map/style-dark -
transit/bus,transit/modes -
dictionary,contacts,faqs directory/departmentsa-to-z/extras-
calendar/named/krlx-schedule,calendar/named/ksto-schedule -
student-work/areas,student-work/wages,student-work/units courses/catalog.dbprinting/color-printersimages/:group/:name-
routes(dev) -
news/mess/wp/v2/*: only if the published manifest points the Messenger there; the bundled and published copies currently do not.
-
wp.stolaf.edu: news, A–Z, TEC calendar. -
olafmessenger.com: Messenger. -
api.presence.io: calendar. -
fa-ewur-saasfaprod1.fa.ocs.oraclecloud.com: jobs. -
www.stolaf.edu:/directory/search,/apps/olecard/checkbalance/*, and the KSTO player page in a web view. -
papercut.stolaf.edu: StoPrint. -
content.krlx.org: now playing. -
cdn.stobcm.com,s3.voscast.com:10803: audio streams. -
carls-app.github.io/map-tiles/style.json: Carleton basemap. -
stolaf.dev/course-data/catalog-recent.db: reached by redirect. -
carleton.frogpond.tech: Carleton's ccc-server. - Tiles, glyphs and sprites named inside the map styles.
- Image URLs embedded in third-party payloads (WordPress media, webcam
thumbnailUrl).
-
Mounting:
-
INSTITUTION=stolaf-college|carleton-collegeserves routes at the root (/v1/...,/ping,/,/_cache). -
INSTITUTION=allserves/stolaf/...and/carleton/...plus a global/ping(ccc-server:source/ccc-server/app.ts:16-39). - Production hosts are single-institution, so
https://stolaf.frogpond.tech/v1/<path>maps to/v1/<path>(inferred fromDEFAULT_URL).
-
-
Middleware order: access log →
koa-compress→conditionalGet→@koa/etag→cacheControlhelper → body parsers → router (app.ts:46-63). The response cache is router-level middleware per institution (ccci-stolaf-college/index.ts:36-38). -
Response cache (
ccc-koa/cache.ts,ccc-server/response-cache.ts):- An LRU of up to 10 000 entries per institution, default max age 1 day.
- Keyed by the full request URL, query string included (
cache.ts:275). - Only GET/HEAD responses with status 200 and a non-empty body are stored, and only for routes that call
ctx.cached(ttl). - Stored with each copy: body, Content-Type, Last-Modified and ETag, plus the
link,x-wp-totalandx-wp-totalpagesheaders (stored-headers.ts). - Concurrent misses for a key share one upstream fetch, gated by
CACHE_FILL_DEDUPE_PERCENT; waiters share errors and non-200 replies too. - Every cached route sends
Vary: Accept-Encoding.
-
Cache-Statusheader (RFC 9211, nameccc-server):-
hit; ttl=<s>on a hit. -
fwd=uri-miss; storedon a fill, withfwd-status=200when it answered 304. -
fwd=uri-miss; collapsed[; stored]for a waiter.
-
-
Cache-Control:-
ctx.cacheControl(ms)setspublic, max-age=<s>, orprivate, no-cache, no-storeforfalse. - On a hit,
max-agecounts down to the copy's remaining life (cache.ts:421-430).
-
-
Conditional GET: every response gets an ETag from
@koa/etag. A request with a matchingIf-None-MatchorIf-Modified-Sincegets304with no body (conditional-get.ts,cache.ts:443-446). The app sendsIf-None-Matchitself only for the course catalog. iOS's sharedURLCache(4 MiB memory / 20 MiB disk, set in the AppDelegate,plugins/with-app-delegate-customizations.ts:30-34) may revalidate or serve other GETs according toCache-Control(inferred). -
Error shapes:
- Koa
ctx.throw/ctx.assertanswer with the status and a plain-text message, for example400 ?id is requiredor404. - Unhandled upstream failures answer
500 Internal Server Error(Koa default, inferred). - The app never parses ccc-server error bodies, except that
fetchSourceBodyreads a WordPresscodewhen the error body is JSON (fetch-source.ts:104-110).
- Koa
-
Retired endpoints answer
200with a one-item "notice" payload rather than a 404:- Titles
Temporarily unavailable/No longer updated/Deprecated endpoint. - Link
https://github.com/frog-pond/ccc-server/discussions/564(ccc-lib/deprecated.ts,feeds/wp-json.ts:98-110,calendar/deprecated.ts:8-24).
- Titles
-
Ping and diagnostics:
-
GET /pinganswerspong; HEAD gives an empty 200. -
GET /answersHello world!. -
GET /_cachelists cache keys with seconds left, itself cached 10 s. -
DELETE /_cache[?key=…]evicts and answers204withX-Cache-Deleted(helpers.ts:80-119). -
GET /v1/routeslists{path, displayName, params}sorted by path; matching is case-insensitive. - The app uses only
routes, in the dev API tester. It never callsping.
-
-
Upstream client:
- ky with User-Agent
ccc-server/0.2.0, 30 s per attempt and 60 s total (ccc-lib/http.ts:4,58-64). - The Messenger proxy times out at 7 s, so a last good copy still beats the app's 10 s timeout (
v1/mess.ts:190-192).
- ky with User-Agent
-
Proxy trust:
X-Forwarded-Foris trusted withmaxIpsCount: 1. A client hang-up mid-response is not reported as an error (behind-proxy.ts,client-abort.ts). -
mDNS (dev): with
ADVERTISE_MDNS=1the server advertises_ccc-server._tcpwith TXTinstitution=<inst>andpath=/v1/, or/forall(server.ts:54-155).
ccc-server endpoint table: St. Olaf (ccc-server:source/ccci-stolaf-college/index.ts)
All routes are GET unless noted. TTL is the server response-cache max age and equals Cache-Control: max-age unless noted. "App" says whether the app calls the route.
Path (/v1/…) |
Query params | Upstream | TTL | Response shape | App |
|---|---|---|---|---|---|
food/item/:itemId |
none | BonApp nutrition for item | 1 h | nutrition JSON | no |
food/menu/:cafeId |
none read (the app adds ?cafeId, which only splits the cache key) |
BonApp cafe by id; known ids 261 stav, 262 cage, 263 kingsRoom, 35 burton, 36 ldc, 34 sayles, 458 weitz; else 400 cafeId must be one of …
|
1 h |
CafeMenuResponseType ({days, items, cor_icons, …}) |
dev picker |
food/cafe/:cafeId |
same | BonApp cafe info | 1 h | CafeInfoResponseType |
dev picker |
food/named/menu/the-pause |
none | stolaf.dev/AAO-React-Native/pause-menu.json |
1 h | {data:{foodItems,stationMenus,corIcons}} |
yes |
food/named/{cafe,menu}/stav-hall |
none | stolaf.cafebonappetit.com/cafe/stav-hall | 1 h | cafe / menu | yes |
food/named/{cafe,menu}/the-cage |
none | …/the-cage | 1 h | yes | |
food/named/{cafe,menu}/kings-room |
none | …/the-kings-room | 1 h | no | |
food/named/{cafe,menu}/the-cave |
none | …/the-cave | 1 h | no | |
food/named/{cafe,menu}/burton |
none | carleton.cafebonappetit.com/cafe/burton | 1 h | yes | |
food/named/{cafe,menu}/ldc |
none | …/east-hall | 1 h | yes | |
food/named/{cafe,menu}/sayles |
none | …/sayles-cafe | 1 h | yes | |
food/named/{cafe,menu}/weitz |
none | …/weitz-cafe | 1 h | yes | |
food/named/{cafe,menu}/schulze |
none | …/schulze-cafe | 1 h | yes | |
calendar/google |
id (required, 400 ?id is required) |
Google Calendar | 1 min | events | no |
calendar/ics |
url (required, must parse) |
any iCal URL | 1 min | events | no |
calendar/named/stolaf |
none | none (retired notice event) | 1 min |
[event] notice |
no (app reads TEC directly) |
calendar/named/oleville |
none | none (retired) | 1 day | notice | no |
calendar/named/northfield |
none | northfieldmn.gov iCal | 1 min | events | no |
calendar/named/krlx-schedule |
none | Google Calendar krlxradio88.1@gmail.com
|
1 min | events (frogpond.events+json) |
yes |
calendar/named/ksto-schedule |
none |
stolaf.dev/AAO-React-Native/ksto-schedule.json expanded to a weekly schedule |
1 min | events | yes |
a-to-z |
none | none (deprecated notice link group) | 1 day | [{title,data:[{label,url}]}] |
no |
a-to-z/extras |
none | GH Pages a-to-z.json
|
1 day | JSON passthrough | yes |
sources |
none | GH Pages sources.json
|
1 day | JRD | yes |
dictionary |
none | GH Pages dictionary.json
|
1 day | {data:[word]} |
yes |
directory/departments |
none | www.stolaf.edu/directory/departments?format=json |
1 day | {results:[{name,…}]} |
yes |
directory/majors |
none | www.stolaf.edu/directory/majors?format=json |
1 day | JSON | no |
contacts |
none | GH Pages contact-info.json
|
1 day | {data:[contact]} |
yes |
tools/help |
none | GH Pages help.json
|
1 day | JSON | no |
faqs |
none | GH Pages faqs.json
|
1 day | {faqs?, text?} |
yes |
webcams |
none | GH Pages webcams.json
|
1 day | {data:[webcam]} |
yes |
images/:group/:name |
none allowed (any query → 404) |
stolaf.dev/AAO-React-Native/img/<group>/<name>. Group must be one of contacts, news-sources, spaces, streaming, webcams; name matches ^[a-z0-9]+(-[a-z0-9]+)*\.webp$, else 404. Upstream 404 → 404 with no Cache-Control; other failure → 502 |
1 day |
image/webp bytes |
yes |
jobs |
none | none (deprecated notice job) | 1 day |
[job] notice |
no |
map |
none | stolaf.dev/campus-map-data/map.json |
1 h | JSON | no |
map/geojson |
none | …/campus-map-data/map.geojson |
1 h | FeatureCollection | yes |
map/categories |
none | GH Pages map-categories.json
|
1 day | {data:{stolaf:{groups,icons},carleton:{…}}} |
yes |
map/style, map/style-dark
|
none |
…/campus-map-data/style.json, style-dark.json
|
1 h | MapLibre style | yes (by URL in MapLibre) |
orgs |
none | Presence api.presence.io/stolaf/v1, without Demo orgs |
36 h | [org] |
yes |
orgs/categories |
none | Presence categories | 36 h | [{category, uris…}] |
yes |
orgs/category-styles |
none | GH Pages org-categories.json
|
1 day | {data:[…]} |
yes |
orgs/uri/:uri |
none; uri must match ^[a-z0-9]+(-[a-z0-9]+)*$, else 404 before the cache |
Presence org page | 36 h | org detail, or 404 No student org has the uri …
|
yes |
student-work/units |
none | Oracle HCM (server-side) plus GH Pages areas | 1 h | {postingId: unit | "other" | null} |
yes |
student-work/areas |
none | GH Pages student-work-areas.json
|
1 day | {data:[area]} |
yes |
student-work/wages |
none | GH Pages student-wages.json
|
1 day | {data:…} |
yes |
courses/catalog.db |
none |
307 redirect to https://stolaf.dev/course-data/catalog-recent.db, Cache-Control: max-age=3600, not response-cached |
n/a | redirect | yes |
news/rss |
url required |
any RSS | 1 h | [FeedItem] |
no |
news/wpjson |
url required |
any WP JSON | 1 h | [FeedItem] |
no |
news/named/stolaf |
none | none (notice) | 1 h |
[FeedItem] notice |
no |
news/named/oleville, politicole, ksto
|
none | none (Deprecated endpoint) |
1 h | [FeedItem] |
no |
news/named/mess |
none | olafmessenger.com posts, per_page 10 | 1 h | [FeedItem] |
no |
news/named/krlx |
none | content.krlx.org/feed/ |
1 h | [FeedItem] |
no |
news/mess/wp/v2/:resource[/:id] |
allow-listed per resource (per_page 1–100, page, _embed, _fields, categories, include ≤100 ids, staff_name, staff_year, hide_empty, slug); unknown or duplicate → 400, unknown resource → 404 |
olafmessenger.com WP REST, 7 s timeout. On outage (timeout, 5xx, 403/408/429, non-JSON) it serves the last good copy for up to 7 days with TTL 1 min; no copy → 502 | posts list 5 min, post 1 h, categories, media, staff, pages 1 day | WordPress JSON with x-wp-total, x-wp-totalpages and RFC 8288 Link paging |
only if the manifest points here |
spaces/hours |
none | GH Pages building-hours.json
|
1 h | {data:[building]} |
yes |
spaces/directory |
none | GH Pages building-directory.json
|
1 h | {data:[directory]} |
yes |
transit/bus |
none | GH Pages bus-times.json
|
1 h | {data:[line]} |
yes |
transit/modes |
none | GH Pages transportation.json
|
1 h | {data:[mode]} |
yes |
streams/upcoming |
dateFrom, dateTo (ISO dates, optional), sort (ascending/descending, default ascending) |
www.stolaf.edu/multimedia/api/collection class=current; starttime converted from America/Chicago to ISO |
1 h | [stream] |
yes |
streams/archived |
same; default the last 2 months | same, class=archived
|
1 h | [stream] |
no |
streams/search |
query (required), sort, class (archived/upcoming/all), category, count, offset, dateFrom, dateTo
|
same | 1 h | streams with Link paging |
no |
printing/color-printers |
none | GH Pages color-printers.json
|
1 day | JSON | yes |
reports/stav |
none | stolaf.dev/stav-mealtimes/two-weeks.json |
1 h | JSON | no |
util/html-to-md |
POST JSON {text} (≤100 kb; non-JSON → 415; missing → 400); GET kept |
none | uncached | Markdown text | no |
athletics/scores |
none |
athletics.stolaf.edu/services/scores_chris.aspx?format=json (team Oles) |
5 min; 1 min while games need frequent refresh |
[Score] with status.indicator
|
yes |
routes |
none | none | uncached | [{path,displayName,params}] |
dev |
"GH Pages" means https://stolaf.dev/AAO-React-Native/<file> (v1/gh-pages.ts).
ccc-server endpoint table: Carleton (ccc-server:source/ccci-carleton-college/index.ts)
"Carls Pages" means https://carls-app.github.io/carls/<file> (v1/gh-pages.ts). The app calls only spaces/hours and map/geojson here.
Path (/v1/…) |
Upstream | TTL / notes | App |
|---|---|---|---|
food/* (same 25 routes as St. Olaf) |
BonApp, same tables | 1 h | no |
calendar/google, calendar/ics
|
as St. Olaf | 1 min | no |
calendar/named/carleton |
carleton.edu calendar feed | 1 min | no |
calendar/named/the-cave |
none (retired notice) | 1 day | no |
calendar/named/stolaf |
none (retired notice) | 1 day | no |
calendar/named/northfield, krlx-schedule, ksto-schedule
|
as St. Olaf | 1 min | no |
calendar/named/upcoming-convos, convos/upcoming
|
carleton.edu convocations feed | 1 min | no |
calendar/named/sumo-schedule |
carleton.edu SUMO feed | 1 min | no |
convos/upcoming/:id, convos/archived
|
podbean feed / convocations page | 6 h | no |
dictionary |
Carls Pages dictionary-carls.json
|
not response-cached (no ctx.cached); max-age=3600
|
no |
faqs |
Carls Pages faqs.json
|
not response-cached; max-age=3600
|
no |
webcams |
Carls Pages webcams.json
|
not response-cached; max-age=3600
|
no |
contacts, tools/help
|
Carls Pages | 1 h | no |
images/:group/:name |
AAO GitHub Pages (shared) | 1 day | no |
jobs |
carleton.edu student-employment WP, without Archived | 1 day | no |
map, map/geojson
|
carls-app.github.io/map-data/map.json, map.geojson
|
1 h | geojson: yes |
orgs |
none (unavailable notice) | 6 h | no |
news/rss, news/wpjson
|
any URL (url required) |
1 h | no |
news/named/nnb |
retired | 6 h | no |
news/named/carleton-now |
carleton.edu news WP | 1 h | no |
news/named/carletonian |
thecarletonian.com feed | 1 h | no |
news/named/krlx |
content.krlx.org feed | 1 h | no |
news/named/covid |
deprecated | 1 h | no |
spaces/hours |
Carls Pages building-hours.json
|
1 h | yes |
transit/bus, transit/modes
|
Carls Pages | 1 h | no |
util/html-to-md (POST/GET), athletics/scores (athletics.carleton.edu), routes
|
as St. Olaf | as St. Olaf | no |
Inventory: AsyncStorage (one store per app; the iOS library is "SharedStorage" under Application Support/)
| Key | Owner | Shape / default | Version / migration | Affects |
|---|---|---|---|---|
REACT_QUERY_OFFLINE_CACHE (library default, inferred) |
query cache | {buster, timestamp, clientState:{queries,mutations}} |
buster = app version + build; maxAge 24 h (inferred) | every non-calendar query's saved data |
persist:root |
redux-persist (source/redux/store.ts:22-28) |
{settings, buildings, courses, _persist:{version,rehydrated}} |
version 3; migrations 2 and 3 | see redux below |
aao::settings:server-address |
source/lib/storage.ts:54-62 |
JSON string; '' means default |
none | API root |
aao::settings:open-links-in-app |
storage.ts:45-52 |
JSON boolean, default true
|
none | Open Links In (In App vs Safari) |
telemetry-consent |
expo-sqlite kv-store, not AsyncStorage (features/telemetry/store.ts:12-18,55-60) |
{state:{enabled:true, deviceId:null}, version:1} |
v1 | Sentry enablement and user id |
radio-preferences |
radio store | {showOnHome:true, logoIndexes:{}, viewedStationId:'ksto'} |
v1 | Home radio bar, logos, last station |
mess-preferences |
Messenger | {lastSign:null, openedStories:[], stainKind:'coffee', photoTone:'auto', keepPhotoStoriesDark:true} |
v2; v1 (sign only) keeps a known zodiac sign | Messenger |
developer-preferences |
developer | {floatingButtonEnabled:false} |
v1 | DebugSwift floating button |
athletics-filter-preferences |
athletics |
{selectedSports:[]} (availableSports not saved) |
v1 | sports filter |
student-work-seen-postings |
Student Work |
{seenIds:null}; an empty list is never saved |
v1 | "new" dots |
student-work-area-layout |
Student Work | {layout:'grid'} |
v1 | grid/list |
student-orgs-category-layout |
Student Orgs | {layout:'list'} |
v1 | grid/list |
quick-actions |
quick actions | {quickActions: DEFAULT_QUICK_ACTIONS} |
v1 | Home Screen quick actions |
news-filter-preferences |
news | {selectedCategories:{}} |
v2; v1 is discarded | per-source category |
map-recent-places |
map | {recent:{stolaf:[],carleton:[]}} |
v1 | recents |
faq-banner-preferences |
FAQs | {dismissed:{[faqId]:{version,dismissedAt}}} |
v1 | banner dismissal |
calendar-filter-preferences |
calendar | {filter:null, mode:'day'} |
v3; v2 selectedCategory becomes {axis:'category', value}
|
calendar filter and mode |
Zustand persist writes {state, version} JSON under the bare name. Stores with hydrated (radio, layouts) set it once a read has finished, even when the read failed (radio/store.ts:157-159, layout-store.ts:45).
The prefixed helper module builds keys as `${prefix}:${key}` with prefix 'aao:', which gives a double colon (modules/storage/index.ts:16, source/lib/storage.ts:14).
storage.ts also defines helpers that nothing calls; they are dead code:
homescreen:view-orderhomescreen:disabled-viewssettings:ackdbuildings:favoritedsis:course-data:*courses:recent-searchescourses:recent-filters
-
settings(parts/settings.ts:27-33):-
unofficialityAcknowledged(false) -
devModeOverride(false) -
enabledCalendarSources:['stolaf','presence'], or['uitest']under UI test -
directoryResultsView:'tiles' - Selectors fall back to the defaults when a field is missing (
:74-78).
-
-
buildings:favorites: [{campus,name}], kept sorted byname.localeCompareon toggle (parts/buildings.ts:34-47). -
courses:-
recentFilters: up to 3, newest first, skipping an empty description or a duplicate description. -
recentSearches: up to 3, de-duplicated case-insensitively (parts/courses.ts:44-52).
-
-
Migrations (
migrations.ts):-
2: adds
presencetoenabledCalendarSources. A missing list becomes the full default list, and an explicit empty list becomes['presence']. -
3: converts bare-string favourites to
{campus:'stolaf', name}.
-
2: adds
-
Sentry: the Redux enhancer sends no state and only the action
type(sentry-enhancer.ts).
-
aao.db(expo-sqlite, Documents/SQLite;source/database/client.ts:101).- Tables:
meta(key PK, value),event,occurrence,event_tag, plus indexes (calendar/schema.ts:20-55). - On open the app runs
pragma foreign_keys = on. -
Rebuild rule:
meta.fingerprint = "<SCHEMA_VERSION=2>|<device IANA zone>". A mismatch dropsvisible_event,event_tag,occurrence,eventandmeta, then recreates them (schema.ts:5,22-67). The check runs once per process. - A database that will not open is deleted and recreated (
client.ts:54-63). - Calendar rows older than 30 days are pruned on write (
calendar/write.ts:16,37-50).
- Tables:
-
Caches/course-catalog.db: attached as schemacatalog. It holdsaao_download(etag)and the FTS5 indexcourse_fts(courses/catalog-file.ts:10,index-build.ts:118). It is downloaded tocourse-catalog.next.db, checked and indexed there, then moved into place. A file that cannot be opened or searched is deleted. -
Caches files: images shared through the share sheet are downloaded into Caches (
components/lib/share-image.ts:13). -
Documents (test or dev only):
fixture-recording.jsonl(Messenger fixture recording),chaos-tape-<n>.jsonl,chaos-findings.jsonl. -
URLCache.shared: 4 MiB in memory, 20 MiB on disk. It holds HTTP responses, including
force-cacheimages.
-
Keychain: internet credentials for server
stolaf.edu(SIS/OleCard username and password;login.ts:15,37-51). -
UserDefaults:
DebugSwiftEnabled(modules/debug-tools/ios/DebugToolsModule.swift:22). -
System: the alternate app icon (react-native-change-icon) and the iOS Home Screen quick actions (synced from the
quick-actionsstore).
| Mechanism | Query cache | AsyncStorage (redux, zustand, aao::) | aao.db and catalog | telemetry kv-store | Keychain | App icon | UserDefaults |
|---|---|---|---|---|---|---|---|
Developer Reset Everything → refreshApp (lib/refresh.ts) |
yes | yes (AsyncStorage.clear()) |
yes | no | yes (stolaf.edu) |
yes, reset to Default | no |
UI-test reset channel (lib/uitest-reset.ts) |
yes | yes | yes | no | no | no | yes (native) |
--reset-state launch argument (AppDelegate) |
yes (file removed) | yes (Application Support/ and RCTAsyncLocalStorage_V1) | yes (Documents/SQLite) | yes (same SQLite folder, inferred) | no | no | yes |
clearStoredData (lib/clear-stored-data.ts:11-32) runs in this order:
- Pause redux-persist, then flush it.
-
queryClient.clear(). -
persister.removeClient(). -
AsyncStorage.clear(). -
dropDatabase(): close the database, deleteaao.db(a "not found" error is not reported), and delete the catalog file. It never throws.
After clearing, refreshApp always restarts the JS app via Restart(), even if the Keychain or icon step fails.
-
Launch order (
app/_layout.tsx:1-17): chaos init (when enabled), constants, logbox, moment, sentry, API roots, query client. Redux rehydration gates the UI behindLoading App.... The query cache restore then runs inside the provider. -
Server URL validity:
- An
http:/https:URL is accepted. Any other text disables Save and showsInvalid URL!. - A missing trailing slash is added on save, because without it ky would replace the last path segment (
constants.ts:4-5). - The new address applies after the automatic restart.
- The Carleton client is unaffected.
- The dev address also applies to
apiUrl()(map style, course catalog, radio relative hrefs) and to images.
- An
-
Relative vs absolute href: absolute means it matches
^[a-z][a-z\d+.-]*:(case-insensitive;fetch-source.ts:11-15). Relative hrefs go throughclient, so they honour the override. Absolute hrefs bypass it. -
Abort: unmounting a screen aborts its in-flight fetches through React Query's signal. The 10 s timer is cleared (
fetch-source.test.ts"unmounting…"). -
Manifest offline:
fetchManifestnever waits for the network. Offline it resolves immediately to the bundled copy (resolve.ts:20-28; testfetchManifest resolves to the bundled document rather than hanging while offline). -
Stale-while-error:
- After a failed refetch, a screen keeps showing the data it had (
listState,menuView). - The cached data is not overwritten on disk: a failed query is not dehydrated, so the previously saved successful copy remains until the next successful write (inferred from the throttled whole-cache writes). The exception is
persistAfterFailure, which is written explicitly as success.
- After a failed refetch, a screen keeps showing the data it had (
-
Restored data is not re-validated by fetch code. This is why the buster drops caches from other builds and why map categories re-validate in
select(map/category-groups-query.ts:83-90,98). - Calendar data is never persisted in the query cache. A cold launch always refetches calendar sources (stale 1 h within a session), and reads come from SQLite. A time-zone change is noticed only at the next cold start, when the database is wiped.
-
Course catalog conditional download (
courses/refresh.ts:72-145):- Concurrent callers share one refresh.
-
If-None-Matchlists the stored ETag and the last rejected ETag. - 304 with a stored copy means unchanged.
- A rejected ETag throws
CatalogRejectedErrorand is not retried. - A non-OK status throws
The course catalog could not be downloaded (HTTP <status>). - Index batches yield to the UI between batches.
- The file swap is atomic with respect to reads.
-
Athletics polling: every 30 s while any game is in play, every 5 min otherwise or with no data (
athletics/query.ts:12-32). This follows the server's own TTL switch from 5 min to 1 min. -
Now playing: polled only while that station is loaded and playing or starting. The interval is the feed's
refreshMs, else 60 s (use-now-playing.ts:37-44). -
Images: requested with
cache: 'force-cache', so once cached they show with no network and are never revalidated. A changed picture must be published under a new name (remote-images.ts:35-44). Prefetch failures are swallowed. -
Directory search query is trimmed.
usernamemaps to theemailparameter (directory/query.ts:15-38). -
BonApp menu keys include the campus day, so a new day fetches fresh even while the previous day's data is fresh (
menus/query.ts:23-36). -
Query failure telemetry (
telemetry/query-failures.ts):- Fires on every query cache
updatedevent whose action iserror, so after React Query's retries. - Sends the Sentry log
api.failurewith{source: first key element if it is a string, else 'unknown'; kind; status}. - Kind and status:
-
SourceFetchError→httpwith its status. - ky
HTTPError→httpwith the response status. - ky
TimeoutError→timeout, 0. - ky
NetworkError→network, 0. - anything else →
other, 0.
-
- Nothing is sent when the person has opted out, because Sentry is closed.
- Fires on every query cache
-
Sentry request data:
- Native network breadcrumbs are off (
enableNetworkBreadcrumbs: false). - JS breadcrumbs, spans and events have query strings and fragments stripped from URLs (
scrub.ts:19-24). -
enableCaptureFailedRequests: true(sentry.ts:39).
- Native network breadcrumbs are off (
-
Course catalog errors are additionally sent to Sentry with
captureException(courses/refresh.ts:132,client.ts:91,135,145). -
The UI test fixtures replace network data for these features:
- orgs
- org categories
- menus (Stav and The Pause)
- athletics
- St. Olaf hours
- bus
- transit modes
- dictionary
- contacts
- directory search
- map
- map categories
- building directory
- jobs
- Student Work areas and wages
- calendar (
uitestsource) - balances
- course catalog (in-memory)
-
Known discrepancy: the app comment says ccc-server reads
cafeIdonly from the query string. The server reads:cafeIdfrom the path (ccc-server:…/v1/menu.ts:52; test/food/menu/:cafeId reads the cafe from the path). The app sends both, so this is harmless; the extra?cafeIdjust splits the server cache key.
- Shared notices use SwiftUI
ContentUnavailableView, so the title, description and action are native accessibility elements. The action is a bordered, large button with the given label (Try Again). - The Developer screen host carries identifier
screen-developer(app/developer/index.tsx:21). - No other accessibility-specific code lives in this area. Dynamic Type follows the native controls.
-
ATS (
app.config.ts:227-236):- Arbitrary loads are allowed in web content.
-
localhostmay use insecure HTTP. -
s3.voscast.comdoes not require forward secrecy. - The ATS exceptions name only
localhost, so plain-HTTP requests to a LAN server found over mDNS (http://<ip>:port) may be blocked by ATS (inferred).
-
Bonjour:
NSBonjourServices: ['_ccc-server._tcp.'], with local-network usage stringUsed in development mode to discover a local ccc-server instance on the same network. -
Dev mode (
useIsDevMode) is a debug build ORsettings.devModeOverride. It gates the Developer screen and mDNS discovery. -
The dev "force bundled data" switch (in memory, not persisted) makes St. Olaf hours and the building directory read the bundled JSON (
building-hours/dev/data-source-store.ts). -
Launch arguments:
-
--uitesting: fixtures, and['uitest']calendar sources. -
--reset-state: native wipe at launch. --record-fixtures-
--uitest-reset-channel <dir>: in-place reset between tests. The JS side renders nothing, clears storage, the native side clears UserDefaults and answers, and the JS restarts. -
--chaos,--chaos-seed,--chaos-launch,--chaos-replay,--chaos-fault-rate,--chaos-profile: wrapfetchwith injected faults: latency 0.5–8 s, status 404/500, network failure, empty, malformed or truncated body, mutated JSON, and timed offline windows (source/chaos/faults.ts,network.ts).
-
-
probe()writes[probe] <ms> <message>to the native log hook in CI, around the UI-test reset (lib/probe.ts,uitest-reset.ts:31-35). -
focusManagerignores AppState on web (tanstack-query.ts:163); not relevant on iOS. - The AppDelegate sets the audio session category to playback (outside this area).
Base URL and client
-
DATA-001 [MUST] With no saved server address, every relative request goes to
https://stolaf.frogpond.tech/v1/<path>. Verify: manual, network inspector;source/lib/__tests__/remote-images.test.ts(default root). Source:source/lib/constants.ts:3,source/init/api.ts:11-19 -
DATA-002 [MUST] Carleton building hours and map geojson are fetched from
https://carleton.frogpond.tech/v1/regardless of the server override. Verify: manual, with the override set, open Hours › Carleton. Source:source/init/api.ts:9,source/features/building-hours/query.ts:31-33,source/features/map/query.ts:22-24 -
DATA-003 [DEV] Saving a server address that does not end in
/stores it with a trailing/. Verify: manual. Source:source/lib/storage.ts:56-59 -
DATA-004 [DEV] Saving the server address restarts the app, and afterwards all relative requests, image URLs and
apiUrl()hrefs use the new root. Verify: manual. Source:source/features/developer/server-url.tsx:40-46,source/lib/api-url.ts:13-15 -
DATA-005 [DEV] An empty server address means the default URL. Verify: manual. Source:
source/init/api.ts:14-16,server-url.tsx:53 -
DATA-006 [DEV] The Save row reads
Invalid URL!and is disabled for non-empty text that is not an http(s) URL. Verify: manual. Source:server-url.tsx:12-19,48-49,72-76 -
DATA-007 [DEV] While the stored address loads, the field is disabled with placeholder
Loading…. Afterwards the placeholder ishttps://stolaf.frogpond.tech/v1/. Verify: manual. Source:server-url.tsx:54-70 -
DATA-008 [DEV] In dev mode, ccc-server instances advertised as
_ccc-server._tcpare listed underLocal Serversashttp://<addr>:<port><txt.path or /v1/>. An IPv6 address is bracketed. Tapping one fills the field. Verify:source/features/developer/__tests__/use-server-discovery.test.ts. Source:use-server-discovery.ts:19-67 -
DATA-009 [DEV] A discovered server that goes away disappears from the list, and discovery stops when the screen unmounts. Verify: use-server-discovery test "removes only the matching discovered server", "cleans up listeners on unmount". Source:
use-server-discovery.ts:51-64 -
DATA-010 [MUST] Image URLs are
<root>images/<group>/<name>.webp, where group is one of contacts, news-sources, spaces, streaming or webcams. Images are requested with force-cache. Verify:remote-images.test.ts,published-images.test.ts. Source:source/lib/remote-images.ts:16-44 -
DATA-011 [SHOULD] Image prefetch fetches each distinct URL once and ignores failures. Verify: remote-images test "fetches each image once…". Source:
remote-images.ts:52-54
Fetching sources
-
DATA-012 [MUST] A source href with a URI scheme is fetched directly. One without a scheme is resolved against the configured API root. Verify:
modules/data-sources/__tests__/fetch-source.test.ts. Source:modules/data-sources/fetch-source.ts:11-15,97-113 -
DATA-013 [MUST] A direct (absolute) source fetch is aborted after 10 seconds. Verify: fetch-source test "an absolute fetch that never resolves is aborted after ten seconds". Source:
fetch-source.ts:3,26-41 -
DATA-014 [MUST] Leaving a screen aborts its in-flight source fetch without waiting for the timeout. Verify: fetch-source test "unmounting… aborts". Source:
fetch-source.ts:29-31 -
DATA-015 [MUST] An error status from any source produces an error whose message is
<label> fetch failed: <status>and which carries the status, plus the WordPresscodewhen the body is JSON with one. Verify: fetch-source tests "carries the code WordPress named" and "carries no code…". Source:fetch-source.ts:47-78,104-121 -
DATA-016 [MUST] Text-format sources (RSS, Atom, iCal) are read as text, whether relative or absolute. Verify: fetch-source "text format" tests. Source:
fetch-source.ts:95-123 -
DATA-017 [MUST] A list response drops individual malformed items. If every item of a non-empty list is malformed, the load fails with
every <what> was malformed. An empty list is an empty state. Verify:parse-each.test.ts. Source:modules/data-sources/parse-each.ts:14-33
Manifest
-
DATA-018 [MUST] The app fetches the sources manifest from
GET sourcesat most once a day per session cache (staleTime 24 h). It does not retry, and it runs even when the device reports offline. Verify: code review;resolve.test.ts. Source:modules/data-sources/resolve.ts:16-37 -
DATA-019 [MUST] When the manifest cannot be fetched or parsed, features resolve their sources from the bundled manifest immediately, including while offline. Verify: resolve test "fetchManifest resolves to the bundled document rather than hanging while offline". Source:
resolve.ts:41-47 -
DATA-020 [MUST] A source missing from the fetched manifest, or of an unsupported type, falls back to its bundled entry. An id with no usable entry anywhere throws, or is dropped from lists. Verify: resolve tests "rule 2", "rule 3", "resolveSources…". Source:
resolve.ts:79-116 -
DATA-021 [MUST] A previously fetched manifest stays saved on disk even when a later refresh fails. Verify:
tanstack-query.test.ts"the sources manifest stays in storage after a refresh fails". Source:resolve.ts:32,tanstack-query.ts:82-84,144 -
DATA-022 [MUST] The bundled manifest carries the entries in the Data table (news stolaf/mess/oleville, A–Z stolaf/extras, calendar stolaf/presence/krlx/ksto, jobs, org categories, map categories, student work ×3, map styles, course catalog, radio). Verify: resolve tests "the bundled manifest is a valid JRD document", "…carries the St. Olaf jobs site", "…offers Presence as a calendar". Source:
modules/data-sources/bundled.json -
DATA-023 [MUST] Manifest links are validated:
relis a URL,hrefis a non-empty URI reference without whitespace, and the id property is required. Verify:types.test.ts. Source:modules/data-sources/types.ts:30-45 -
DATA-024 [MUST] Radio sources use the cached manifest however old it is, and wait (render nothing) only while the saved cache is being restored. Verify: code review. Source:
modules/data-sources/use-manifest.ts:18-25 -
DATA-025 [MUST] A radio player page is loaded only from
https://www.stolaf.edu. Any other published host falls back to the shipped page. Verify: code review. Source:source/features/streaming/radio/sources.ts:25,96-116
Query cache and persistence
-
DATA-026 [MUST] Unused query data is kept in memory for 24 hours. Verify: code review. Source:
source/init/tanstack-query.ts:20-28 -
DATA-027 [MUST] Successful query data is saved to device storage and shown on the next launch before any refetch completes. Verify: manual, airplane mode relaunch shows cached Menus. Source:
tanstack-query.ts:90-148,app/_layout.tsx:112 -
DATA-028 [MUST] Failed and pending queries are not saved, except those that opt in. Verify: tanstack-query test "still refuses a query that has not succeeded". Source:
tanstack-query.ts:145 -
DATA-029 [MUST] Calendar-ingest and calendar-read results (keys starting
calendar) are never saved to the query cache on disk. Radio broadcast schedules (schedule) are. Verify: tanstack-query tests "shouldDehydrateQuery…", "still persists a broadcast schedule". Source:tanstack-query.ts:110-113,140 -
DATA-030 [MUST] Queries marked
persist: false(course reads, now playing, Messenger lead text, non-front Messenger issues) are never saved. Verify: tanstack-query test "stays out of storage with…". Source:tanstack-query.ts:142 -
DATA-031 [MUST] Paged lists that opt in save only their first page, as loaded, even after a later page failed. Verify: tanstack-query tests "is written with only its first pages…", "persists its pages after a further page fails". Source:
tanstack-query.ts:75-88,143 -
DATA-032 [MUST] A saved query cache written by a different app version or build, or with no version stamp, is discarded at launch. Verify: tanstack-query tests "a cache saved by another build…". Source:
tanstack-query.ts:117-121 -
DATA-033 [SHOULD] A saved query cache older than 24 hours is discarded at launch (library default maxAge). Verify: manual / library test. Source:
tanstack-query.ts:115-148(no maxAge override) -
DATA-034 [MUST] Pull-to-refresh on a paged list reloads from page 1 and drops later pages, which reload as the list scrolls. Verify: code review. Source:
source/lib/infinite-data.ts:28-34 -
DATA-035 [MUST] When the app returns to the foreground, stale queries on visible screens refetch. Verify: manual. Source:
tanstack-query.ts:162-166 -
DATA-036 [MUST] When NetInfo reports a connection again, paused fetches resume and stale queries refetch. While NetInfo reports no connection, fetches are paused rather than failed. Verify: manual, toggle airplane mode. Source:
tanstack-query.ts:155-159
Offline and error UI
-
DATA-037 [MUST] A screen with saved data keeps showing it when a refetch fails. Verify:
modules/notice/__tests__/list-state.test.ts"shows the list while a refetch fails…". Source:modules/notice/list-state.ts:19 -
DATA-038 [MUST] A first load that fails with nothing saved shows
Couldn’t Load, the error's message (orSomething went wrong.) and aTry Againbutton that retries. Verify:NoticeView.test.tsx. Source:modules/notice/notice.tsx:70-99 -
DATA-039 [MUST] A first load waiting for a connection, with the device offline, shows the
Offlinenotice (wifi.slash) rather than a spinner. A pause while online keeps the spinner. Verify: list-state tests "says it is offline…", "keeps loading, not offline…". Source:list-state.ts:21-24 -
DATA-040 [MUST] An offline menu with nothing saved shows
Offline/You're offline, and this menu hasn't been saved on this phone yet. It will load once you're back online.Verify:ModuleMenusTests(manual offline). Source:source/features/menus/lib/menu-view.ts:35-49 -
DATA-041 [MUST] Student Work offline with no saved board shows
Offline/Student Work needs a connection to load the job board the first time.withTry Again. Verify: manual. Source:app/student-work/index.tsx:23,84-93 -
DATA-042 [MUST] Course search offline with no downloaded catalog shows
Offline/Course search needs a connection to download the course catalog the first time.Verify: manual. Source:source/features/sis/course-search/lib/list-state.ts:5-6,45 -
DATA-043 [SHOULD] The app shows no global offline banner. Offline is surfaced per screen only. Verify: manual. Source:
source/init/tanstack-query.ts:155-159(only NetInfo consumer)
Per-feature request contract
-
DATA-044 [MUST] The Student Orgs list requests
GET orgs, considered fresh for 5 minutes. Verify:ModuleStudentOrgsTests(fixture). Source:source/features/student-orgs/query.ts:16-32 -
DATA-045 [MUST] Org detail requests
GET orgs/uri/<percent-encoded uri>once, with no retry. A 404 shows the list's data alone, without an error. Verify: code review. Source:student-orgs/query.ts:45-75 -
DATA-046 [MUST] BonApp menus and cafe info are requested from
food/named/{menu,cafe}/<slug>for stav-hall, the-cage, burton, ldc, sayles, weitz and schulze. They are cached per campus day and fresh for 1 hour. Verify:ModuleMenusTests;menu-bonapp.test.tsx. Source:source/features/menus/query.ts:28-36,46-64,119-142,app/menus/*.tsx,app/carleton-*-menu.tsx -
DATA-047 [MUST] The Pause menu is requested from
food/named/menu/the-pauseand read from.data. Verify:ModuleMenusTests(bundled under UI test). Source:menus/query.ts:164-174 -
DATA-048 [MUST] Athletics scores refetch every 30 seconds while any game is in play, and every 5 minutes otherwise. Verify: code review. Source:
source/features/athletics/query.ts:12-32 -
DATA-049 [MUST] Building hours come from
spaces/hourson each campus's own server, cached separately per campus. Verify:ModuleHoursTests. Source:building-hours/query.ts:21-61 -
DATA-050 [MUST] Map buildings come from
map/geojsonon each campus's own server, fresh for 1 hour (5 minutes when read by a building card). Verify:ModuleMapTests. Source:map/query.ts:12-40,map/card-queries.ts:9-25 -
DATA-051 [MUST] St. Olaf building directories come from
spaces/directory. They are never requested for Carleton. Verify: code review. Source:map/directory/fetch.ts:16-22,card-queries.ts:35-40 -
DATA-052 [MUST] The St. Olaf basemap style URL comes from the manifest (
map/styleormap/style-darkby system appearance), resolved against the API root. Carleton always useshttps://carls-app.github.io/map-tiles/style.jsonin light mode. Verify: code review. Source:map/style-query.ts:27-45,map/urls.ts:12,19-24 -
DATA-053 [MUST] A map-categories file of an unreadable shape fails without retry and leaves the previous table showing. A restored table of an unreadable shape is ignored. Verify: code review. Source:
map/category-groups-query.ts:67-104 -
DATA-054 [MUST] Transit uses
transit/busandtransit/modes. Dictionary usesdictionary(5 min). Contacts usecontacts(5 min). FAQs usefaqs. Webcams usewebcams. All are read from.datawhere shown. Verify:ModuleTransitTests,ModuleCampusDictionaryTests,ModuleDirectoryTests. Source: respectivequery.tsfiles -
DATA-055 [MUST] Departments come from
directory/departments(.results), sorted alphabetically by name and fresh for 1 day. Verify:ModuleDirectoryTests. Source:directory/departments-query.ts:12-31 -
DATA-056 [MUST] Directory search calls
https://www.stolaf.edu/directory/search?format=json&<field>=<trimmed query>directly, with field firstname, lastname, department, major, query, title or email (for username). Results are fresh for 1 minute. Verify:ModuleDirectoryTests(fixture). Source:directory/query.ts:8-68 -
DATA-057 [MUST] Upcoming streams request
streams/upcoming?sort=ascending&dateFrom=<today campus>&dateTo=<+2 months>. Verify:ModuleStreamingMediaTests. Source:streaming/streams/query.ts:13-37 -
DATA-058 [MUST] KRLX and KSTO schedules come from
calendar/named/krlx-scheduleandcalendar/named/ksto-schedule, hide events that have already ended, and are saved for offline. Verify:ModuleStreamingMediaTests. Source:modules/ccc-calendar/query.ts:161-181 -
DATA-059 [MUST] The St. Olaf campus calendar is fetched directly from TEC for today through the same day next month. Presence is fetched directly from api.presence.io. Both are written to SQLite and are fresh for 1 hour. Verify:
ModuleCalendarTests(fixture). Source:modules/ccc-calendar/query.ts:60-146 -
DATA-060 [MUST] News (St. Olaf, Messenger), the A–Z index and the job board are fetched directly from their hosts as named by the manifest. Verify:
ModuleNewsTests,ModuleStudentWorkTests. Source:news/query.ts:42-54,more/query.ts:33-57,modules/ccc-jobs/query.ts:38-90 -
DATA-061 [MUST] A failure loading A–Z extras does not blank the A–Z index. A missing upstream A–Z source fails with
no A–Z index source. Verify: code review. Source:more/query.ts:39-55 -
DATA-062 [MUST] Now Playing is requested only while its station is loaded and playing or starting. It is re-polled at the feed's
refreshMs(default 60 s) and never saved. Verify: code review. Source:radio/use-now-playing.ts:31-44 -
DATA-063 [MUST] Posting units come from
student-work/units(1 h). Areas and wages come from the manifest'sstudent-work/*sources (5 min) and are schema-validated. Verify:ModuleStudentWorkTests(fixtures). Source:modules/ccc-jobs/query.ts:102-117,student-work/areas-query.ts,wages-query.ts -
DATA-064 [MUST] OleCard balances log in by POSTing username and password to
https://www.stolaf.edu/apps/olecard/checkbalance/authenticate.cfm, then GET…/api.cfm. A redirect URL containingerror=means login failed. Verify: manual. Source:source/lib/login.ts:53-80,lib/financials/balances.ts:23-36 -
DATA-065 [MUST] StoPrint talks directly to
https://papercut.stolaf.edu/rpc/api/rest/internal/with form-encoded bodies andOrigin: https://papercut.stolaf.edu. Color printers come from ccc-serverprinting/color-printers. Verify:ModuleStoPrintTests. Source:source/lib/stoprint/urls.ts,api.ts:103
Course catalog
-
DATA-066 [MUST] The course catalog is requested from the manifest's
courses/catalog.db, resolved against the API root, withIf-None-Matchlisting the stored and last-rejected ETags. A 304 keeps the stored file. Verify:source/database/courses/__tests__/refresh.test.ts. Source:source/database/courses/refresh.ts:17-27,72-98 -
DATA-067 [MUST] A downloaded catalog that fails its check is rejected by ETag and not downloaded or retried again in this session. Other failures retry up to 3 times. Verify: refresh.test. Source:
refresh.ts:43-45,52-53,90-92,110-115 -
DATA-068 [MUST] A new catalog replaces the old one only after it is fully checked and indexed. Searches never see a missing catalog during the swap. Verify: refresh.test. Source:
refresh.ts:101-130 -
DATA-069 [MUST] The catalog is checked for updates at most hourly and is not saved in the query cache. Verify: code review. Source:
source/database/courses/read.ts:42-49
SQLite
-
DATA-070 [MUST] The app database
aao.dbis rebuilt empty whenever the schema version or the device time zone differs from what it was built with. The check happens at the first use after a cold start. Verify:source/database/schema.test.ts. Source:source/database/schema.ts:50-67,client.ts:33-35,67 -
DATA-071 [MUST] A database that cannot be opened is deleted and recreated, with no error shown. Verify: code review. Source:
client.ts:57-63 -
DATA-072 [MUST] A course catalog file that cannot be opened or searched is deleted and downloaded again on the next refresh. Verify: client.test "discards a catalog file it cannot search", "still opens when an unsearchable catalog cannot be deleted". Source:
client.ts:80-99 -
DATA-073 [MUST] The catalog file lives in Caches (not backed up, purgeable). Verify: code review. Source:
courses/catalog-file.ts:4-11 -
DATA-074 [MUST] Calendar rows older than 30 days are removed on each write. Verify:
calendar/write.test.ts. Source:calendar/write.ts:16,37-50
Persisted settings
-
DATA-075 [MUST] The Open Links In choice is stored under
aao::settings:open-links-in-app(default In App). Verify:customize/__tests__/open-links-in.test.ts. Source:source/lib/storage.ts:45-52 -
DATA-076 [MUST] Telemetry consent and device id are stored synchronously (expo-sqlite kv-store
telemetry-consent) and are readable before the first render. The default is enabled with no id. Opting out forgets the id. Opting in creates a new one. Verify:telemetry/__tests__/store.test.ts. Source:features/telemetry/store.ts:12-62 -
DATA-077 [MUST] Redux settings, building favourites and course recents persist under
persist:rootat version 3. Verify:redux/__tests__/migrations.test.ts. Source:source/redux/store.ts:22-28 -
DATA-078 [MUST] Upgrading from redux version 1 or 2 adds Presence to enabled calendars. A list that never existed becomes the full default. An explicitly empty list gains only Presence. Verify: migrations tests. Source:
redux/migrations.ts:21-52,93 -
DATA-079 [MUST] Upgrading from redux version ≤2 converts favourites stored as names into St. Olaf favourites. Verify: migrations tests "the campus-scoped favourites migration". Source:
migrations.ts:61-88,94 -
DATA-080 [MUST] Building favourites are keyed by campus and name and kept sorted by name. Verify:
redux/__tests__/buildings.test.ts. Source:redux/parts/buildings.ts:34-47 -
DATA-081 [MUST] Recent course searches keep the 3 newest, de-duplicated case-insensitively. Recent filters keep the 3 newest by description, ignoring empty ones. Verify: code review. Source:
redux/parts/courses.ts:27-52 -
DATA-082 [MUST] Settings saved before a field existed read that field's default (calendar sources, directory view
tiles). Verify:redux/parts/__tests__/settings.test.ts. Source:parts/settings.ts:45-78 -
DATA-083 [MUST] Each Zustand-persisted preference survives relaunch under its key with the defaults in the Persisted state table. Verify:
ModuleStreamingMediaTests.testSwitchOffHidesOnlyHomesBar(radio);layout-store.test.ts. Source: store files listed -
DATA-084 [MUST] Calendar filter state saved by v2 (
selectedCategory) migrates to{axis:'category', value}. Verify: calendar store test. Source:features/calendar/store.ts:37-42 -
DATA-085 [MUST] News filters saved by v1 are discarded. Messenger prefs from v1 keep only a valid zodiac sign. Verify: news and mess store tests. Source:
features/news/store.ts:19-24,features/mess/store.ts:42-46 -
DATA-086 [MUST] A layout screen draws nothing layout-dependent until its saved layout has loaded. Verify: layout-store test "is not hydrated until…". Source:
source/lib/layout-store.ts:11-16,45 -
DATA-087 [MUST] The seen Student Work postings list is never overwritten with an empty list. Verify: code review. Source:
sis/student-work/store.ts:19-24 -
DATA-088 [MUST] The athletics filter saves only the selected sports, not the available ones. Verify: code review. Source:
athletics/store.ts:39-46 -
DATA-089 [MUST] SIS credentials are stored in the Keychain under server
stolaf.eduand are read fresh on every use (never cached). Verify: manual. Source:source/lib/login.ts:15,32-51,82-88
Reset
-
DATA-090 [DEV]
Reset EverythingasksAre you sure you want to clear everything?withNope!andReset it!. Verify: manual. Source:features/developer/developer.tsx:12-21 -
DATA-091 [DEV] Confirming the reset empties the in-memory query cache and the saved query cache, clears all AsyncStorage, deletes
aao.dband the catalog file, removes thestolaf.eduKeychain item, restores the default app icon, and restarts. Verify:clear-stored-data.test.ts,database/__tests__/client.test.ts. Source:source/lib/refresh.ts:7-23,clear-stored-data.ts:11-32 -
DATA-092 [MUST] A settings change made just before or during a reset does not survive it. Verify: clear-stored-data tests "leaves behind no setting changed…". Source:
clear-stored-data.ts:14-19 -
DATA-093 [MUST] The reset restarts the app even if clearing the Keychain or resetting the icon fails. Verify: code review. Source:
refresh.ts:10-22 -
DATA-094 [MUST] Dropping a database that was never created, or that fails to close, does not abort the reset. Verify: client.test "does not throw when there is no database to delete", "…nothing was ever opened". Source:
client.ts:101-151 -
DATA-095 [DEV] Under
--reset-state, AsyncStorage, the SQLite folder and UserDefaults are cleared before JS starts. Verify:plugins/with-app-delegate-customizations.test.ts. Source:plugins/with-app-delegate-customizations.ts:11-28 -
DATA-096 [DEV] The UI-test reset channel clears stored data in place, clears UserDefaults natively, then restarts at the requested route. Verify: every
UITestCasesubclass (reset between tests). Source:source/lib/uitest-reset.ts:20-40
Telemetry and privacy for network failures
-
DATA-097 [MUST] Each query that finally fails (after retries) sends one
api.failurelog. Its attributes are the key's first element (orunknown), the kind (http/network/timeout/other) and the status (0 when none). Verify:telemetry/__tests__/query-failures.test.ts. Source:features/telemetry/query-failures.ts:14-51,app/_layout.tsx:92 -
DATA-098 [MUST] No request URL query string or fragment reaches Sentry, and native network breadcrumbs are disabled. Verify:
init/__tests__/scrub.test.ts,sentry-options.test.ts. Source:source/init/scrub.ts:19-95,sentry-options.ts:63-71 -
DATA-099 [MUST] Redux state is never attached to Sentry events. Only action types are. Verify:
redux/__tests__/sentry-enhancer.test.ts. Source:source/redux/sentry-enhancer.ts:6-14
Developer tools
-
DATA-100 [DEV] The API Tester lists every server route from
GET routes, grouped by first path segment. Opening one fetches it fresh (no caching) and shows raw or highlighted JSON. Verify: manual. Source:features/developer/api-test/query.ts:15-30,app/developer/api-test/detail.tsx:25-35 -
DATA-101 [DEV] The BonApp picker accepts digits only and loads
food/menu/<id>?cafeId=<id>. With no id it showsNo Café ID. Verify: manual. Source:app/developer/bon-app-picker.tsx:13-46
ccc-server contract
-
API-001 [MUST] Every route in the St. Olaf and Carleton tables exists at
/v1/<path>on a single-institution deployment, and at/<stolaf|carleton>/v1/<path>inallmode. Verify: ccc-serversource/ccc-server/app.test.ts,ccci-stolaf-college/v1/routes.test.ts"${route} is registered". Source:ccc-server:source/ccc-server/app.ts:24-39 -
API-002 [MUST]
GET /pinganswerspong(HEAD: empty 200) andGET /answersHello world!. Verify: app.test "all mounts both institutions…". Source:ccc-server:source/ccc-server/helpers.ts:19-25 -
API-003 [MUST]
GET /v1/routesreturns[{path, displayName, params}]sorted by path, matches case-insensitively, and answers 405 to POST. Verify: app.test "mixed-case route listings…". Source:ccc-server:source/ccci-stolaf-college/index.ts:178-189 -
API-004 [MUST] Cached routes answer from an in-memory cache keyed by full URL (including query string), up to 10 000 entries per institution, for the per-route TTLs in the tables. Verify:
ccc-koa/cache.test.ts, app.test "deleting one cache key preserves other query variants". Source:ccc-server:source/ccci-stolaf-college/index.ts:36,ccc-koa/cache.ts:275,392-451 -
API-005 [MUST] Only GET/HEAD 200 responses with a body are cached. 4xx/5xx are never cached. Verify: cache.test "a response fetched but not stored…". Source:
ccc-server:source/ccc-koa/cache.ts:541-562 -
API-006 [MUST] Responses carry
Cache-Control: public, max-age=<seconds>per route. On a cache hit, max-age counts down to the copy's remaining life. Verify: cache.test "a hit says in Cache-Control how long its copy has left…". Source:ccc-server:source/ccc-koa/ctx-cache-control.ts,cache.ts:421-430 -
API-007 [MUST] Responses carry an ETag. A matching
If-None-Matchgets 304 with no body, both on fill and on hit. Verify: cache.test "a 304 served from the cache is still a hit". Source:ccc-server:source/ccc-server/app.ts:52-53,ccc-koa/conditional-get.ts,cache.ts:443-446 -
API-008 [SHOULD] Responses carry
Cache-Status: ccc-server; …describing hit, stored or collapsed. Verify: cache.test "…says so in Cache-Status". Source:ccc-server:source/ccc-koa/cache.ts:289-292,431-441,491-497 -
API-009 [MUST] Concurrent misses for one URL share one upstream fetch, and its errors. Verify: cache.test "concurrent misses for one key share one upstream fetch", "when the first request fails…". Source:
ccc-server:source/ccc-koa/cache.ts:308-377 -
API-010 [MUST] Cached copies return the
Link,x-wp-totalandx-wp-totalpagesheaders. Verify: cache.test "a hit gives back the headers…". Source:ccc-server:source/ccc-lib/stored-headers.ts -
API-011 [MUST]
GET /v1/sourcesreturns the published JRD manifest (GH Pagessources.json), cached for 1 day. Verify: routes.test. Source:ccc-server:source/ccci-stolaf-college/v1/sources.ts -
API-012 [MUST]
GET /v1/courses/catalog.dbanswers 307 tohttps://stolaf.dev/course-data/catalog-recent.dbwithmax-age=3600, and the redirected host answersIf-None-Match. Verify: manual curl. Source:ccc-server:source/ccci-stolaf-college/v1/courses.ts:17-21 -
API-013 [MUST]
GET /v1/images/:group/:nameservesimage/webpfor published groups and.webpnames. A query string, unknown group or bad name gets 404 without fetching upstream. A missing upstream file gets 404 with no Cache-Control. Upstream failure gets 502. Verify:ccc-lib/images.test.ts. Source:ccc-server:source/ccc-lib/images.ts:11-66 -
API-014 [MUST]
GET /v1/orgs/uri/:urirejects slugs not matching^[a-z0-9]+(-[a-z0-9]+)*$with 404 before the cache. An unknown or Demo org gets 404No student org has the uri <uri>. Verify: routes.test "/orgs/uri/:uri …". Source:ccc-server:source/ccci-stolaf-college/v1/orgs.ts:29-48 -
API-015 [MUST]
/v1/orgs,/v1/orgs/categoriesand/v1/orgs/uri/:uriare cached for 36 hours. Demo orgs are excluded. Verify: routes.test. Source:orgs.ts:11-27 -
API-016 [MUST]
/v1/food/menu/:cafeIdand/v1/food/cafe/:cafeIdread the id from the path and answer 400cafeId must be one of …for unknown ids. Verify:menu-routes.test.ts. Source:ccc-server:source/ccci-stolaf-college/v1/menu.ts:27-72 -
API-017 [MUST] Named BonApp cafe and menu routes proxy the cafebonappetit.com pages in the table, cached for 1 hour. Verify:
menu.test.ts. Source:menu.ts:15-25,83-207 -
API-018 [MUST]
/v1/athletics/scorescaches for 5 minutes, and for 1 minute while games need frequent refresh. Verify:athletics/index.test.ts. Source:ccc-server:source/ccci-stolaf-college/v1/athletics.ts:12-24 -
API-019 [MUST]
/v1/streams/upcomingaccepts optionaldateFrom/dateTo(ISO date) andsort(default ascending), asks St. Olaf's multimedia API forclass=current, and returns starttimes as ISO instants converted from America/Chicago. Verify: routes.test "/streams/upcoming and searching upcoming streams use the same default window". Source:ccc-server:source/ccci-stolaf-college/v1/streams.ts:103-137 -
API-020 [MUST] GH-Pages-backed data routes (
contacts,dictionary,faqs,webcams,printing/color-printers,a-to-z/extras,map/categories,orgs/category-styles,student-work/areas,student-work/wages) proxyhttps://stolaf.dev/AAO-React-Native/<file>and cache for 1 day.spaces/hours,spaces/directory,transit/busandtransit/modescache for 1 hour. Verify: routes.test. Source:ccc-server:source/ccci-stolaf-college/v1/*.ts -
API-021 [MUST]
/v1/map/geojson,/v1/map/styleand/v1/map/style-darkproxyhttps://stolaf.dev/campus-map-data/…with a 1-hour cache. Carleton'smap/geojsonproxieshttps://carls-app.github.io/map-data/map.geojson. Verify: routes.test. Source:ccc-server:source/ccci-stolaf-college/v1/map.ts,ccc-server:source/ccci-carleton-college/v1/map.ts:12-17 -
API-022 [MUST] Carleton
/v1/spaces/hoursproxieshttps://carls-app.github.io/carls/building-hours.jsonwith a 1-hour cache and the same schema as St. Olaf's. Verify: app.test "endpoints can add dotted and major versions…". Source:ccc-server:source/ccci-carleton-college/v1/hours.ts -
API-023 [MUST]
/v1/student-work/unitsreturns a map from posting id to unit ("other"when unlisted), cached for 1 hour. Verify:student-work/units.test.ts. Source:ccc-server:source/ccci-stolaf-college/v1/student-work.ts:28-38 -
API-024 [MUST]
/v1/calendar/named/krlx-scheduleproxies KRLX's Google Calendar, andksto-scheduleexpands the published weekly KSTO schedule. Both cache for 1 minute. Verify:calendar.test.ts,weekly-schedule.test.ts. Source:ccc-server:source/ccci-stolaf-college/v1/calendar.ts:53-68 -
API-025 [MUST] The Messenger WordPress proxy refuses unknown resources (404) and unknown, duplicate or malformed parameters (400) without contacting the paper. Verify:
mess.test.ts"rulesFor…". Source:ccc-server:source/ccci-stolaf-college/v1/mess.ts:116-136 -
API-026 [MUST] During a Messenger outage (timeout >7 s, 5xx, 403/408/429, non-JSON) the proxy serves the last good copy (up to 7 days old) with a 1-minute TTL, or 502 when it has none. Verify: mess.test "serves the last good copy on …", "is a 502 on an outage with no copy". Source:
mess.ts:190-318 -
API-027 [MUST] Paged Messenger lists carry an RFC 8288
Linkheader (first, prev, next, last) and WordPress total headers. Verify: mess.test "paginationLinks". Source:mess.ts:153-170,238-250 -
API-028 [MUST] Retired routes answer 200 with a one-item notice payload (title
Temporarily unavailable,No longer updatedorDeprecated endpoint, linking discussion #564), not 404. Verify:calendar/deprecated.test.ts. Source:ccc-server:source/ccci-stolaf-college/v1/deprecated.ts,feeds/wp-json.ts:98-110 -
API-029 [MUST] Bad requests answer with the status and a plain-text message, for example
400 ?url is required. Verify: routes.test. Source:ccc-server:source/ccci-stolaf-college/v1/news.ts:14-25,calendar.ts:17-29 -
API-030 [SHOULD] Upstream requests identify as
ccc-server/0.2.0and time out at 30 s per attempt and 60 s in total. Verify:ccc-lib/http.test.ts. Source:ccc-server:source/ccc-lib/http.ts:4,58-64 -
API-031 [SHOULD]
DELETE /_cache[?key=…]evicts entries (all or the named keys) and answers 204 withX-Cache-Deleted.GET /_cachelists keys and seconds left. Verify: app.test "all keeps cached responses separate…". Source:ccc-server:source/ccc-server/helpers.ts:27-52 -
API-032 [SHOULD] A client hanging up mid-response is not reported as a server error. Verify:
client-abort.test.ts. Source:ccc-server:source/ccc-koa/client-abort.ts -
API-033 [DEV] With
ADVERTISE_MDNS=1the server advertises_ccc-server._tcpwith TXTinstitutionandpath(/v1/, or/inallmode). Verify: manual. Source:ccc-server:source/ccc-server/server.ts:54-155 -
API-034 [SHOULD] Carleton
dictionary,faqsandwebcamsare not response-cached (noctx.cached) but still sendmax-age=3600. Verify: code review. Source:ccc-server:source/ccci-carleton-college/v1/dictionary.ts:6-9,faqs.ts,webcams.ts
| Test | What it proves | Covers |
|---|---|---|
source/init/__tests__/tanstack-query.test.ts (shouldDehydrateQuery; meta persist; sources manifest; cache saved by another build) |
dehydration rules, page trimming, persistAfterFailure, buster | DATA-021, 028–032 |
source/lib/__tests__/clear-stored-data.test.ts |
redux writes cannot survive a clear; AsyncStorage empty after | DATA-091, 092 |
source/lib/__tests__/layout-store.test.ts |
layout defaults, remembering, hydrated
|
DATA-083, 086 |
source/lib/__tests__/remote-images.test.ts, published-images.test.ts
|
image URL against the root; prefetch de-dup and failure; groups match published | DATA-001, 010, 011 |
source/database/__tests__/client.test.ts |
dropDatabase closes, deletes the catalog, tolerates missing or failing; bad catalog discarded | DATA-072, 091, 094 |
source/database/schema.test.ts |
fingerprint rebuild | DATA-070 |
source/database/courses/__tests__/refresh.test.ts, catalog-file.test.ts
|
conditional download, rejection, swap | DATA-066–068, 073 |
source/database/calendar/write.test.ts |
retention | DATA-074 |
source/redux/__tests__/migrations.test.ts |
redux v2/v3 migrations | DATA-077–079 |
source/redux/__tests__/buildings.test.ts |
campus-keyed favourites, sorting | DATA-080 |
source/redux/parts/__tests__/settings.test.ts |
calendar source defaults, directory view fallback | DATA-082 |
source/redux/__tests__/sentry-enhancer.test.ts |
no state to Sentry | DATA-099 |
modules/data-sources/__tests__/fetch-source.test.ts |
relative vs absolute, timeout, abort, error mapping, text format | DATA-012–016 |
modules/data-sources/__tests__/resolve.test.ts |
bundled manifest contents; rules 1–3; offline fallback | DATA-019, 020, 022 |
modules/data-sources/__tests__/types.test.ts |
JRD validation | DATA-023 |
modules/data-sources/__tests__/parse-each.test.ts |
tolerant list parsing | DATA-017 |
modules/storage/__tests__/storage.test.ts |
getters' defaults | DATA-075 (partially) |
modules/notice/__tests__/list-state.test.ts, NoticeView.test.tsx
|
list, error, offline and loading decision; Try Again; describeError | DATA-037–039 |
source/features/telemetry/__tests__/query-failures.test.ts |
api.failure mapping and subscription | DATA-097 |
source/features/telemetry/__tests__/store.test.ts |
consent store sync storage and ID lifecycle | DATA-076 |
source/init/__tests__/scrub.test.ts, sentry-options.test.ts
|
URL scrubbing; breadcrumb options | DATA-098 |
source/features/developer/__tests__/use-server-discovery.test.ts |
mDNS URL building, removal, cleanup | DATA-008, 009 |
plugins/with-app-delegate-customizations.test.ts |
--reset-state clears SQLite and AsyncStorage |
DATA-095 |
uitests/UITestCase.swift (reset channel used by every test) |
in-place reset between tests | DATA-096 |
uitests/ModuleStreamingMediaTests.swift testSwitchOffHidesOnlyHomesBar
|
a persisted radio preference survives relaunch | DATA-083 |
uitests/ModuleMenusTests, ModuleHoursTests, ModuleMapTests, ModuleDirectoryTests, ModuleStudentOrgsTests, ModuleStudentWorkTests, ModuleTransitTests, ModuleCampusDictionaryTests, ModuleCalendarTests, ModuleNewsTests, ModuleStreamingMediaTests, ModuleStoPrintTests
|
the screens render the data these requests feed (fixtures under --uitesting, so the request contract itself is not exercised) |
DATA-044–065 (screen side only) |
uitests/Chaos/ChaosTests.swift |
the app survives injected network faults | DATA-036–039 (robustness) |
ccc-server source/ccc-server/app.test.ts
|
mounting, ping, routes, _cache, per-institution caches, versions |
API-001–003, 022, 031 |
ccc-server source/ccc-koa/cache.test.ts
|
sharing, Cache-Status, stored headers, max-age countdown, 304 | API-004–010 |
ccc-server ccci-stolaf-college/v1/routes.test.ts
|
registration, orgs/uri, streams params and paging, html-to-md | API-011, 014, 015, 019, 020, 029 |
ccc-server ccci-stolaf-college/v1/menu-routes.test.ts, menu.test.ts
|
cafeId from path; unknown ids refused | API-016, 017 |
ccc-server ccci-stolaf-college/v1/mess.test.ts
|
Messenger allow-list, paging links, last-good-copy | API-025–027 |
ccc-server ccc-lib/images.test.ts
|
image proxy | API-013 |
ccc-server ccc-lib/http.test.ts
|
upstream outcome metrics, timeouts | API-030 |
ccc-server ccc-koa/client-abort.test.ts
|
hang-ups ignored | API-032 |
ccc-server ccci-carleton-college/v1/routes.test.ts
|
Carleton html-to-md, convos id | API-001 |
No automated coverage:
- DATA-002–007 (server override UI and behaviour)
- DATA-018, 024, 025 (manifest timing; radio sources)
- DATA-026, 027, 033–036 (gcTime, restore at launch, maxAge, focus and online refetch)
- DATA-040–043 (exact offline copy is not asserted by any UI test)
- DATA-045, 048, 053, 056 (request shape), 057, 061, 062, 064, 065, 069, 071
- DATA-081, 087–090, 093, 100, 101
- API-012, 018 (TTL switch is tested only at the library level), 023, 024, 028 (partly), 033, 034
-
Telemetry consent survives "Reset Everything". The consent store uses the expo-sqlite kv-store, which neither
AsyncStorage.clear()nordropDatabase()(which deletes onlyaao.db) removes.--reset-statedoes remove it, because it deletes the whole SQLite folder. Is the survival intended? -
The
clientis undefined until the async server-address read finishes (source/init/api.ts:11-21). A query that runs before then would throwCannot read properties of undefined. The persisted-cache restore and redux gate probably delay first renders enough, but nothing guarantees it. -
The query-cache persister key and maxAge are library defaults (
REACT_QUERY_OFFLINE_CACHE, 24 h). node_modules was not available to confirm them. Because the maxAge equalsgcTime, a cache last written more than 24 h ago is discarded entirely, so "offline after a weekend" shows no saved menus. Is that intended? -
ATS vs mDNS: discovered local servers are
http://<LAN IP>URLs, but the ATS exceptions cover onlylocalhost. Saving one may fail every request unless the debug build addsNSAllowsLocalNetworking. -
mDNS in
allmode: the server advertisespath=/, so the app would usehttp://host:port/and request/sources, which 404s;allmode needs/stolaf/v1/. -
Stale app comment:
source/features/menus/query.ts:42-44says the server readscafeIdfrom the query string. The server reads the path, and?cafeId=only makes a separate cache key. The server'sCAFE_ID_TO_URLalso lacks The Cave and Schulze ids. -
Dead code: these
source/lib/storage.tshelpers have no callers: homescreen order and disabled views,settings:ackd,buildings:favorited,sis:course-data:*,courses:recent-*. A native rewrite should not migrate those keys unless older installs still need them. Redux owns favourites and recents now. -
Many ccc-server routes are unused by this app:
- St. Olaf:
food/item, kings-room, the-cave,calendar/google/ics/northfield/stolaf/oleville,a-to-z,directory/majors,tools/help,jobs,map,news/*(all named feeds),streams/archived,streams/search,reports/stav,util/html-to-md. - Carleton: everything except
spaces/hoursandmap/geojson. -
news/named/olevilleis in the manifest but no screen uses it. - These exist for older builds or other clients.
- St. Olaf:
-
Double HTTP caching: iOS
URLCache.shared(20 MiB disk) may serve ccc-server GETs withinmax-agewithout a network round-trip, on top of React Query. If so, a pull-to-refresh within, say, a 1-day TTL could return the cached copy. This was not verified on device. -
Error copy is raw:
LoadErrorViewshowserror.message, for exampleNews fetch failed: 500or ky'sRequest failed with status code …text. A rewrite matching "exact strings" would need ky's message format (inferred). -
Retry stacking: ky's own retries (inferred: 2) run inside each React Query retry (3), so one failing GET through
clientcan make up to 12 attempts beforeapi.failureis reported. -
The Carleton
dictionary,faqsandwebcamsroutes do not use the response cache, so every request hits GitHub Pages. This looks like an oversight; the app does not call them.
← 09. Streaming media: radio, webcams, schedules, movie · Spec index