This file provides guidance to agents when working with code in this repository.
Build / run / test (Maven wrapper, Java 21):
./mvnw clean compile— compile./mvnw spring-boot:run— run locally on http://127.0.0.1:8585 (websocket on 8589)./mvnw test— run all tests./mvnw test -Dtest=GradeTest— run a single test class./mvnw test -Dtest=GradeTest#methodName— run a single test method./mvnw package— producetarget/spring-0.0.1-SNAPSHOT.jar
Docker (mirrors prod): docker compose up -d --build (binds 127.0.0.1:8585 and :8589, mounts ./volumes).
Database migration scripts (Python; need a venv and ADMIN_PASSWORD in .env):
python scripts/db_init.py— wipe + reseed local SQLite fromModelInitpython scripts/db_prod2local.py— pull prod data into local SQLite via APIpython scripts/db_local2prod.py— push local SQLite to prod via API (requires prod admin password)python scripts/db_mysql2local.py/db_local2mysql.py— direct MySQL ↔ SQLite sync (setFORCE_YES=truefor non-interactive)
Entity-removal helpers (run before any large cleanup):
./audit2.sh— scans code + DB for usage of legacy entities (User, StudentResponse, etc.) and prints a SAFE-TO-DELETE verdict./delete.sh— removes only the entitiesaudit2.shcleared; rerun./mvnw clean compileafter
- DB migrations/scripts and
.envsetup: README.md and scripts/ - Auth/JWT cookie flow:
JwtApiControllersetsjwt_java_springon POST/authenticate;JwtRequestFilterreads it on/api/**(local cookie overrides in.env, see README.md) - WebSocket endpoints: native
/websocketin src/main/java/com/open/spring/mvc/mortevision/nativesocket/WebSocketConfig.java; group chat STOMP/ws-chatwith/app+/topicin src/main/java/com/open/spring/mvc/groups/WebSocketBrokerConfig.java and port gating in src/main/java/com/open/spring/mvc/groups/ChatWebSocketPortFilter.java - Frontend templates/static assets: src/main/resources/templates/ (per-domain + layouts) and src/main/resources/static/; Backend UI notes in README.md
- Testing/build verification: commands above + Verification section; run
./mvnw clean compileand./mvnw testbefore schema or auth changes
- Course groups are the canonical uppercase list
CSA, CSP, CSH, CSSE, configured ascourses.class-groupspluscourses.periods[NAME]in application.properties and bound by CourseGroupProperties.java. That one config block drives both theGroupsrows seeded byModelInitand the class listClassGroupMembershipServicevalidates against, so course names and bell periods only need changing in one place. A single period can be overridden in.env(courses.periods[CSA]=4) because Spring merges map entries per key, whereas overridingcourses.class-groupsreplaces the entire list. List order matters — it is the order memberships are returned in. Startup fails if a listed group has no period. Profile class selection syncs viaPUT /api/groups/class-memberships(GroupsApiController→ClassGroupMembershipService.syncMemberships), which only adds/removes membership in those four groups and leaves all other groups untouched. - Realtime chat uses STOMP over a second connector on port 8589: broker config in
WebSocketBrokerConfig.java(/ws-chat,/app,/topic), port gating inChatWebSocketPortFilter.java, presence inGroupChatPresenceService.java. The native/websocketendpoint inmvc/mortevision/nativesocket/WebSocketConfig.javais separate. - Gotcha: Java package is
mvc.groups(plural) but templates live intemplates/group/(singular); static JS exists both as legacy files instatic/js/group-*.jsand packaged versions instatic/js/group/*.js— prefer the latter.
Single Spring Boot app, root package com.open.spring, three layers:
system/— cross-cutting infra.Mainis the entry point.DatabaseModeEnvironmentPostProcessorruns before the context starts and rewritesspring.jpa.properties.hibernate.dialect+ JDBC driver based on whetherDB_URLlooks like SQLite or MySQL — this is how the same JAR runs on dev (SQLite) and prod (MySQL).DatabaseInitializeris a standalone resetter used byscripts/db_init.py.ModelInitseeds default users/data on startup.MvcConfigholds CORS, theBCryptPasswordEncoderbean, and static/file-upload resource handlers.security/— cookie-based JWT auth.POST /authenticate(JwtApiController) returns the JWT in an httpOnlyjwt_java_springcookie (12h, SameSite=None in prod).JwtRequestFilterreads that cookie on every/api/*request and populates the SecurityContext viaPersonDetailsService.RateLimitFiltercaps 1000 req/min/IP (5000 for admins). There is no Authorization-header bearer flow.mvc/— ~70 feature subpackages (person, groups, jokes, bathroom, assignments, quiz, games, blackjack, plant, S3uploads, …). The convention per domain is JPA@Entity+*JpaRepository+*ApiController(REST under/api/...) and optionally*ViewController(Thymeleaf under/) and a service class for non-trivial logic. Follow this pattern when adding a new domain.
Two ports are intentional: 8585 is the main HTTP/Tomcat connector; 8589 is a second connector wired to the websocket handler at mvc/mortevision/nativesocket/WebSocketConfig (group chat realtime). Nginx (nginx_spring_8585_8589.conf) terminates TLS for both: 443→8585, 8589→8589 with websocket upgrade headers and 86400s timeouts.
Hibernate runs with ddl-auto=none and globally quoted identifiers (because tables like groups are SQL reserved words). Schema changes are not auto-applied — they go through DatabaseInitializer / the migration scripts.
Thymeleaf templates live under src/main/resources/templates/ with one folder per domain plus a layouts/ folder for shared fragments. Static assets are in src/main/resources/static/.
application.properties carries production-safe defaults; .env (gitignored, loaded by java-dotenv) overrides them locally. The required keys are listed in the README's .env block — at minimum ADMIN_PASSWORD, DEFAULT_PASSWORD, and (for local dev) jwt.cookie.secure=false, jwt.cookie.same-site=Lax. CORS in application.properties allows *.opencodingsociety.com plus localhost on 4500/4599/4600/8585.
The SQLite DB lives at volumes/sqlite.db (WAL mode); volumes/backups/ holds timestamped backups. Both volumes/sqlite.db* and .env are gitignored.
When changing schema or seed data, follow this order — out-of-order steps will diverge prod and local:
- Local:
python scripts/db_init.py(verify schema migrates cleanly) - Local:
python scripts/db_prod2local.py(test against real data) - Test thoroughly
- On prod (cockpit, in the spring directory):
cp volumes/sqlite.db volumes/backups/sqlite_YYYY-MM-DD.db,docker compose down,git pull,python scripts/db_init.py,docker compose up -d --build - Local: swap
ADMIN_PASSWORDto the prod value in.env, thenpython scripts/db_local2prod.py
After meaningful changes, run all of:
./mvnw clean compile— catches Lombok/annotation-processor breakage./mvnw test./mvnw spring-boot:run, thencurl http://127.0.0.1:8585/api/jokes/(smoke test, no auth required) and authenticate viaPOST /authenticatewith{"uid":"toby","password":"<ADMIN_PASSWORD>"}to confirm the JWT cookie flow still works
To ensure consistency in this repository, agents should adhere to the following conventions:
- Testing: The project relies on plain JUnit 5 and Mockito (
@Mock,@InjectMocks) for unit tests. Spring context testing (e.g.,@WebMvcTest,@SpringBootTest) is generally avoided. Prefer isolating layers and testing logic directly using Mockito. - Entity & Code Style: There is no enforcement via Checkstyle or Spotless. However, classes (especially entities) heavily utilize Lombok (
@Data,@NoArgsConstructor,@AllArgsConstructor). Avoid generating manual getters, setters, or standard constructors for entities. - Naming/Keywords:
spring.jpa.properties.hibernate.globally_quoted_identifiers=trueis enabled. You can safely use SQL reserved words (likegroups) as table or column names, as Hibernate will quote them automatically. - Deployment: There are no GitHub Action workflows currently managing CI/CD. The application isolates deployment to
docker-compose up -d --buildleveraging the rootDockerfileand mounting./volumesto persist SQLite data and backups locally.