ForgeGrid is a gamified Java desktop app (Swing/AWT) that turns coding practice into an RPG‑style journey. Users register/login, complete tasks, gain XP, level up, and can add personal “Goated Tasks.” Data persists in a Railway‑hosted MySQL database via JDBC.
Tech essentials:
- Java 17+
- Swing/AWT for UI
- JDBC (mysql-connector-j) for persistence
- No JavaFX/SWT; only Swing/AWT
Directory highlights:
src/main/java/com/forgegrid/app→ app entry.../ui→ all Swing UI frames/panels and shared UI utilities.../auth,.../service,.../db→ backend/auth/services and DB helpers.../model→ simple POJOs.../config→ env and local user prefssrc/main/resources→ icons/configbuild.bat,build.sh,run.bat→ build/run scripts
- App starts at
com.forgegrid.app.Main→ sets system Look & Feel and showsAuthUIon EDT. AuthUIhandles login/signup and onboarding prompt. After success, it opensDashboardin the same window.Dashboardrenders the main app: navigation, player stats (level/XP), hardcoded tasks, “Goated Tasks,” history, and customization entry.- Services (
AuthService,UserService,LevelService,HardcodedTaskService) talk to MySQL throughDatabaseHelper. - Tables are auto-created/migrated at startup.
-
Entry:
AuthUIextendsJFrame- Card-based navigation (
CardLayout) for: WELCOME, LOGIN, SIGNUP, ONBOARDING_PROMPT, LOADING. - Uses custom panels/components:
WelcomeUI,LoadingScreen,OnboardingInAppPanel(new/returning flows)- (Removed legacy custom background components)
CardContainerPanel: layout helper for consistent card lookTheme: brand colors (no gradients)
- Login/Signup fields are basic Swing inputs (simple borders, placeholders handled inline).
- After login, either:
- Shows
ONBOARDING_PROMPT→ in-app onboarding (OnboardingInAppPanel) → saves onboarding to DB → opensDashboardin-card; or - If already onboarded → shows welcome-back →
Dashboard.
- Shows
- Card-based navigation (
-
Main app:
DashboardextendsJFrame- Layout:
- Sidebar (navigation): Home, Tasks (Assigned/Completed/Skipped/Goated), Profile, Settings, etc.
- Center area (
CardLayout) swaps views. XP bar and streak are displayed; level/XP pulled fromLevelService.
- Data-driven sections via services:
- Hardcoded task list based on onboarding language/skill using
HardcodedTaskService#getTasksForUser - Completion/skip log writes into
user_tasks, and XP updates viaLevelService#addXP - Goated Tasks: CRUD UI hooks that call
HardcodedTaskServicemethods
- Hardcoded task list based on onboarding language/skill using
- Overlays removed; plain card switches (CardLayout).
- Layout:
-
Shared UI utilities
-
Theme: brand colors; gradients removed -
TaskPopupDialog,LoadingScreen(simplified),CardContainerPanel
Layouts used (selection):
BorderLayout,BoxLayout,CardLayout,FlowLayout, and custom-painted panels.
-
DatabaseHelper- Loads Railway config from
.envor system env viaEnvironmentConfig. - Builds JDBC URL, opens connections, and initializes tables and indexes.
- Tables created/migrated:
users: id, username, email, password (SHA‑256), total_xp, level, onboarding_* fields, created/updated timestamps.user_preferences: per-username customization (experience_level, work_style, productivity_goals, notification_preference, customize_completed), timestamps.user_tasks: assigned/completed/skipped + Goated fields (title/description/deadline/xp/is_completed), timestamps/indexes.
- Loads Railway config from
-
AuthService- register(username, email, password): SHA-256 hash; insert into
usersif unique. - login(usernameOrEmail, password): verifies credentials; returns
PlayerProfilewith onboarding fields fromusers. - usernameExists / usernameOrEmailExists
- resetPassword(username, newPassword)
- register(username, email, password): SHA-256 hash; insert into
-
UserService- hasCompletedOnboarding / by username
- saveOnboardingData / by username
- getOnboardingData / by username
- updateLastLogin
- Preferences: save/get/exists (
user_preferences), hasCompletedCustomization - getUserProfileDetails / updateUserProfileDetails
- updateUserScore (legacy score field)
-
LevelService- XP curve: XP(N) = 100 * 1.5^(N-1) for each level increment
- calculateLevelFromXP, getCurrentLevelXP, getRequiredXPForLevel
- addXP(username, delta): updates
users.total_xpandusers.levelatomically; returnsLevelUpResult - getLevelInfo(username): returns level, totalXP, currentLevelXP, requiredForNextLevel
-
HardcodedTaskService- Initializes
user_tasksand migrates Goated columns. - Provides hardcoded task catalogs by language+level (Beginner/Intermediate/Advanced).
- Records assigned/completed/skipped tasks with timestamps and XP.
- Net/total XP, counts, recorded names; auto-skip overdue assigned tasks with XP penalty.
- Goated Tasks: create/list/update/delete; completion credits XP via
LevelService.
- Initializes
PlayerProfile: runtime user profile (username, email, level, score, onboarding fields, timestamps).HardcodedTask: name, description, language, level, xpReward, estimatedMinutes.GoatedTask: id/title/description/deadline/xp/isCompleted/createdAt.TaskHistoryEntry: simple row projection for history lists.
EnvironmentConfig: loads.envand system env; supportsRAILWAY_MYSQL_URLor component variables:RAILWAY_MYSQL_HOST,RAILWAY_MYSQL_PORT,RAILWAY_MYSQL_DATABASE,RAILWAY_MYSQL_USERNAME,RAILWAY_MYSQL_PASSWORD
.env(not in repo) can supply the above. Example keys:- RAILWAY_MYSQL_URL=mysql://user:pass@host:port/db
UserPreferences: local per-device preferences file in user home (forgegrid.prefs), used for last username and “remember me”.
-
users- id PK, username UNIQUE, email UNIQUE
- password (SHA‑256 hex), total_xp INT DEFAULT 0, level INT DEFAULT 1
- onboarding_completed TINYINT(1), onboarding_goal/lang/skill VARCHAR(255)
- created_at/updated_at timestamps
- Indexes on username, email, onboarding_completed
-
user_preferences- id PK, username FK→users(username)
- experience_level, work_style, productivity_goals, notification_preference
- customize_completed BOOLEAN, created_at/updated_at
- Index on username
-
user_tasks- id PK, username, task_name, time_taken, xp_earned, status, completed_at
- Goated: type('goated'|'regular'), title, description, deadline, xp, is_completed, created_at
- Indexes on username, status, type
-
Windows:
build.bat- Recursively lists all Java files to
sources.txt, compiles with Java 17 (--release 17) tobinwith classpathlib/*. - Copies
src/main/resources/**intobin/(ensures icons are present). - Run after successful build:
run.bat.
- Recursively lists all Java files to
-
Windows:
run.bat- Sets classpath:
bin;lib\*and runscom.forgegrid.app.Main.
- Sets classpath:
-
Linux/macOS:
build.sh- Compiles with
javac -cp "lib/*" -d bin --release 17using explicit source globs for packages. - Copies
src/main/resources/*intobin/.
- Compiles with
Notes:
- Ensure
lib/mysql-connector-j-8.0.33.jarexists. - Provide
.envor system env vars for Railway credentials before running.
- Launch app (
run.bator IDE).Mainsets L&F and showsAuthUI. - Register/Login in
AuthUI. Credentials are hashed with SHA‑256 and verified viaAuthService/userstable. - If new user or not onboarded:
OnboardingInAppPanelcollects goal/language/skill, andUserServicesaves tousers. AuthUIswitches content toDashboard(same frame).Dashboardloads:- Level/XP via
LevelService#getLevelInfo - Tasks via
HardcodedTaskService#getTasksForUser - Completed list via
HardcodedTaskService#getCompletedTasks/history queries
- Level/XP via
- Completing a task →
HardcodedTaskService#saveCompletedTaskandLevelService#addXP. - Skipping a task →
saveSkippedTaskwith negative XP;LevelService#addXPreflects penalties (auto-skip handles overdue assigned tasks too). - Goated Tasks → CRUD via
HardcodedTaskService, completion credits XP. - Customization/preferences →
UserService.saveUserPreferencesstored inuser_preferences.
- All UI is Swing/AWT; no JavaFX.
- DB schema auto-creation/migration is done at startup; safe on re-run.
- XP system is centralized in
LevelServicewith a scalable curve. - Local
UserPreferenceskeeps UX conveniences device-specific; sensitive data stays in MySQL.
- Entry:
app/Main.java - Auth/UI:
ui/AuthUI.java,ui/WelcomeUI.java,ui/LoadingScreen.java,ui/OnboardingInAppPanel.java - Main UI:
ui/Dashboard.java, shared:ui/Theme.java,ui/CardContainerPanel.java - Services:
auth/AuthService.java,service/UserService.java,service/LevelService.java,service/HardcodedTaskService.java - DB:
db/DatabaseHelper.java - Models:
model/PlayerProfile.java,model/HardcodedTask.java,model/GoatedTask.java,model/TaskHistoryEntry.java - Config:
config/EnvironmentConfig.java,config/UserPreferences.java - Resources:
resources/com/forgegrid/icon/logo2_transparent.png,resources/config.properties - Scripts:
build.bat,build.sh,run.bat
- Install JDK 17+.
- Place
mysql-connector-j-8.0.33.jarinlib/(already in repo). - Provide Railway credentials via
.envor environment variables. - On Windows: double-click
build.bat, thenrun.bat. On macOS/Linux:bash build.shthen run from IDE orjava -cp "bin:lib/*" com.forgegrid.app.Main.
That’s the complete picture of ForgeGrid as it stands: UI, services, database, scripts, and flows.
- Where:
auth/AuthService.java, UI inui/AuthUI.java - Hashing: Passwords are hashed using SHA‑256 before storage and verification.
- Method:
AuthService#hashPassword(String)→MessageDigest.getInstance("SHA-256"), hex-encodes hash bytes.
- Method:
- Registration flow:
- UI validates fields (non-empty) →
AuthService.register(username, email, password). - Guard:
usernameOrEmailExists(value)ensures global uniqueness (username and email cannot collide cross-field). - Hash password →
INSERT INTO users (username, email, password). - DB constraints: UNIQUE on
usernameandemailenforce uniqueness at SQL layer too.
- UI validates fields (non-empty) →
- Login flow:
- UI collects
usernameOrEmail,password→ hash entered password. - Query:
SELECT id, username, email, onboarding_completed, onboarding_goal, onboarding_language, onboarding_skill FROM users WHERE (username = ? OR email = ?) AND password = ?
- On success, build
PlayerProfileand populate onboarding fields. - UI decides next step: either onboarding prompt or straight to dashboard.
- UI collects
- Reset password:
UPDATE users SET password = ? WHERE username = ?(hashed).
Security notes:
- Hash-only (no salt/pepper). For production hardening, add per-user salt and stronger KDF (bcrypt/Argon2/PBKDF2) and avoid Base64-storing plaintext for remember-me (see 13.7).
- Where:
db/DatabaseHelper.java,config/EnvironmentConfig.java - Configuration loading:
EnvironmentConfigmerges.envand system env. Supports either a fullRAILWAY_MYSQL_URL(converted to JDBC ifmysql://) or individual parts (RAILWAY_MYSQL_HOST,..._PORT,..._DATABASE,..._USERNAME,..._PASSWORD).
- Connection:
- Driver:
com.mysql.cj.jdbc.Driver(loaded viaClass.forName). - JDBC URL: built from env or converted from
mysql://...tojdbc:mysql://...?...withuseSSL=true&serverTimezone=UTC&allowPublicKeyRetrieval=true. getConnection()lazily opens and reuses a singleConnectioninstance when needed.
- Driver:
- Initialization:
- On helper construction: open connection →
createUsersTable()→createUserPreferencesTable()→createIndexes(). - Migrations are idempotent:
CREATE TABLE IF NOT EXISTS ...;ALTER TABLEguarded by try/catch to ignore “already exists.” - Indexes created in try/catch blocks to be safe across repeated runs.
- On helper construction: open connection →
Tables auto-created:
userswithtotal_xp,level, and onboarding fields; created/updated timestamps withON UPDATE CURRENT_TIMESTAMP.user_preferenceswithcustomize_completedand FK tousers(username).user_tasks(created inHardcodedTaskService) with status tracking and Goated columns; indexes onusername,status,type.
- Where:
ui/AuthUI.java,ui/OnboardingInAppPanel.java,service/UserService.java - New users after login:
- If
hasCompletedOnboardingByUsername(username)is false,AuthUIshowsONBOARDING_PROMPT→ user chooses to proceed now or skip. - Proceed:
OnboardingInAppPanel(new-user mode) asks Q1/Q2/Q3 via aCardLayoutflow. - On completion:
UserService.saveOnboardingDataByUsername(username, goal, language, skill)setsonboarding_completed=1and stores choices onusers. - Skip:
saveOnboardingDataByUsername(username, "Skipped", "Not specified", "Not specified")and move on.
- If
- Returning users:
OnboardingInAppPanelin returning mode shows “Welcome back” and immediately continues;UserService.getOnboardingDataByUsername(username)can preload values.
- Dashboard task selection uses onboarding
languageandskillto load appropriate hardcoded tasks.
- Where:
service/HardcodedTaskService.java,ui/Dashboard.java - Hardcoded catalogs:
- Methods:
getBeginnerTasks(lang),getIntermediateTasks(lang),getAdvancedTasks(lang)returnList<HardcodedTask>per language+level. getTasksForUser(language, level)picks the correct list.
- Methods:
- Assignment tracking:
- When a task is surfaced,
recordAssignedTask(username, taskName)writes anassignedrow (if not already present), storing assigned time incompleted_atfor aging checks.
- When a task is surfaced,
- Completion:
saveCompletedTask(username, taskName, timeTaken, xpEarned)inserts acompletedrow with XP.- After save,
LevelService.addXP(username, xpEarned)updatesusers.total_xpandusers.level.
- Skipping:
saveSkippedTask(username, taskName, timeTaken, xpLost)inserts askippedrow with negative XP.LevelService.addXP(username, xpLost)applies penalty to total XP.
- Auto-skip expired assigned tasks:
autoSkipExpiredAssignedTasks(username, language, level)convertsassignedrows older than 24h toskippedwith XP penalty of 50% of the task’s reward.- Penalty is credited through
LevelService.addXPto keep UI in sync with net XP.
- History and stats:
getCompletedTasks(username)andgetTaskHistory(username, limit)read back rows.getTotalXP,getNetXP,getCompletedTaskCount,getSkippedTaskCountprovide aggregates for UI.
- Where:
service/HardcodedTaskService.java,model/GoatedTask.java,ui/Dashboard.java - Schema additions in
user_tasks(migrated if missing):type,title,description,deadline,xp,is_completed,created_at, index ontype. - Create:
createGoatedTask(username, title, description, deadline, xp)→ inserts a row withtype='goated'andstatus='assigned'. - List:
listGoatedTasks(username)returns ordered list (unfinished first, then by deadline). - Update/Delete:
updateGoatedTask(...),deleteGoatedTask(...)operate on the same table. - Complete:
markGoatedTaskComplete(username, taskId)setsis_completed=1,status='completed', writesxp_earned=xp, timestamps completion, then callsLevelService.addXP(username, xp)to update level progression.
- Where:
service/LevelService.java - XP curve per level step:
getRequiredXPForLevel(N) = round(100 * 1.5^(N-2))for N≥2. - Given
total_xp:calculateLevelFromXP(totalXP)iteratively finds the highest level where total XP covers all prior steps.getCurrentLevelXP(totalXP)subtracts XP for prior levels to show in-level progress.
- Adding XP:
addXP(username, xpDelta)readsusers.total_xp, adds delta, recalculates level, and writes backtotal_xp,level, andupdated_at.- Returns
LevelUpResult(leveledUp flag, old/new levels, totals, progress) for UI.
- Reading level info:
getLevelInfo(username)returns snapshot for UI (current progress and requirement for next level).
- Where:
config/UserPreferences.java, used byAuthUI - Stored file:
${user.home}/forgegrid.prefsas JavaProperties. - Keys:
last.username,remember.me,saved.username,saved.password. - Saved password encoding: Base64 (for convenience only; not secure). If “Remember Me” is unchecked, credentials are cleared.
- Security recommendation: Prefer storing tokens/secrets encrypted or avoid password storage entirely.
- Where:
ui/Dashboard.java - On open:
- Reads level/XP via
LevelService.getLevelInfo(username)and setscurrentLevel,currentXP,maxXP. - Loads hardcoded tasks based on onboarding
language/skillviagetTasksForUser. - Loads completed tasks via
getCompletedTasksand history viagetTaskHistory. - Renders views via
CardLayoutin the center panel; sidebar navigation drives card switching. - XP bar is a custom-painted
JPanelthat readscurrentXP/maxXPand repaints on updates.
- Reads level/XP via
- Actions (complete/skip/customize) call into corresponding service methods; after writes, UI refreshes by re-querying services.
build.batandbuild.shcompile with--release 17and classpathlib/*, output tobin/.- Resource copy ensures
src/main/resources/**ends up inbin/; images (e.g.,logo2_transparent.png) are loaded viagetResource. run.batsets CP=bin;lib\*then startscom.forgegrid.app.Main.
- Provide either:
RAILWAY_MYSQL_URL=mysql://user:pass@host:port/database(recommended), orRAILWAY_MYSQL_HOST,RAILWAY_MYSQL_PORT,RAILWAY_MYSQL_DATABASE,RAILWAY_MYSQL_USERNAME,RAILWAY_MYSQL_PASSWORD.
.envcan hold these values locally;EnvironmentConfigreads.envand system env and builds a JDBC URL.
- Entry/UI boot:
app/Main.java→ui/AuthUI.java
- Auth:
auth/AuthService.java(hashing, register/login queries)config/UserPreferences.java(remember me)
- Onboarding:
ui/AuthUI.java(flows),ui/OnboardingInAppPanel.java(Q1/2/3 UI)service/UserService.java(save/get onboarding)
- Dashboard:
ui/Dashboard.java(views, calls to controllers/services)ui/Theme.java
- Tasks and XP:
service/HardcodedTaskService.java(assignment, complete/skip, goated, history)service/LevelService.java(XP curve and updates)
- Database & Config:
db/DatabaseHelper.java(init/migrations/indices)config/EnvironmentConfig.java(env/.env merge)
- Models:
model/PlayerProfile.java,model/HardcodedTask.java,model/GoatedTask.java,model/TaskHistoryEntry.java
- Scripts & Resources:
build.bat,build.sh,run.batsrc/main/resources/com/forgegrid/icon/logo2_transparent.png
Authentication (SHA-256 hashing):
private String hashPassword(String password) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes());
// Convert byte array to hex string
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
System.err.println("SHA-256 algorithm not available: " + e.getMessage());
e.printStackTrace();
return null;
}
}Authentication (login query with username/email OR match):
String selectSQL = """
SELECT id, username, email, onboarding_completed, onboarding_goal,
onboarding_language, onboarding_skill
FROM users
WHERE (username = ? OR email = ?) AND password = ?
""";Authentication (registration insert):
String insertSQL = "INSERT INTO users (username, email, password) VALUES (?, ?, ?)";
try (Connection conn = dbHelper.getConnection();
PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {
pstmt.setString(1, username.trim());
pstmt.setString(2, email.trim());
pstmt.setString(3, hashedPassword);
int rowsAffected = pstmt.executeUpdate();
return rowsAffected > 0;Environment config (convert mysql:// URL to JDBC):
private static String convertRailwayUrlToJdbc(String railwayUrl) {
try {
// Remove mysql:// prefix
String url = railwayUrl.substring(8);
// Find @ symbol to separate credentials from host
int atIndex = url.indexOf('@');
if (atIndex == -1) {
throw new IllegalArgumentException("Invalid Railway URL format");
}
// Extract credentials (user:pass)
String credentials = url.substring(0, atIndex);
String hostAndDb = url.substring(atIndex + 1);
// Find : in credentials to separate user and password
int colonIndex = credentials.indexOf(':');
if (colonIndex == -1) {
throw new IllegalArgumentException("Invalid Railway URL format - no password");
}
String username = credentials.substring(0, colonIndex);
String password = credentials.substring(colonIndex + 1);
// Find : in hostAndDb to separate host and port
int portIndex = hostAndDb.indexOf(':');
if (portIndex == -1) {
throw new IllegalArgumentException("Invalid Railway URL format - no port");
}
String host = hostAndDb.substring(0, portIndex);
String portAndDb = hostAndDb.substring(portIndex + 1);
// Find / to separate port and database
int slashIndex = portAndDb.indexOf('/');
if (slashIndex == -1) {
throw new IllegalArgumentException("Invalid Railway URL format - no database");
}
String port = portAndDb.substring(0, slashIndex);
String database = portAndDb.substring(slashIndex + 1);
return String.format("jdbc:mysql://%s:%s/%s?useSSL=true&serverTimezone=UTC&allowPublicKeyRetrieval=true",
host, port, database);
} catch (Exception e) {
return railwayUrl;
}
}Database helper (build URL and connect):
private String buildRailwayMySQLUrl() {
String fullUrl = EnvironmentConfig.getRailwayUrl();
if (fullUrl != null && !fullUrl.isEmpty()) {
return fullUrl;
}
return String.format("jdbc:mysql://%s:%s/%s?useSSL=true&serverTimezone=UTC&allowPublicKeyRetrieval=true",
dbHost, dbPort, dbName);
}public Connection getConnection() throws SQLException {
if (connection == null || connection.isClosed()) {
connection = DriverManager.getConnection(dbUrl, dbUsername, dbPassword);
}
return connection;
}Database helper (users table DDL):
String createTableSQL = """
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
total_xp INT DEFAULT 0,
level INT DEFAULT 1,
onboarding_completed TINYINT(1) DEFAULT 0,
onboarding_goal VARCHAR(255) NULL,
onboarding_language VARCHAR(255) NULL,
onboarding_skill VARCHAR(255) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
""";Onboarding (save by username):
public boolean saveOnboardingDataByUsername(String username, String goal, String language, String skill) {
String updateSQL = """
UPDATE users
SET onboarding_completed = 1,
onboarding_goal = ?,
onboarding_language = ?,
onboarding_skill = ?,
updated_at = ?
WHERE username = ?
""";try (Connection conn = dbHelper.getConnection();
PreparedStatement pstmt = conn.prepareStatement(updateSQL)) {
pstmt.setString(1, goal);
pstmt.setString(2, language);
pstmt.setString(3, skill);
pstmt.setString(4, LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
pstmt.setString(5, username);
int rowsAffected = pstmt.executeUpdate();
if (rowsAffected > 0) {
System.out.println("Onboarding data saved successfully for username: " + username);
return true;
}Tasks (create user_tasks DDL):
private void createUserTasksTable() {
String createTableSQL =
"CREATE TABLE IF NOT EXISTS user_tasks (" +
"id INT AUTO_INCREMENT PRIMARY KEY, " +
"username VARCHAR(100) NOT NULL, " +
"task_name VARCHAR(255) NOT NULL, " +
"time_taken INT, " +
"xp_earned INT, " +
"status VARCHAR(50) DEFAULT 'assigned', " +
"completed_at TIMESTAMP NULL, " +
// Goated tasks support
"type VARCHAR(20) DEFAULT 'regular', " +
"title VARCHAR(255) NULL, " +
"description TEXT NULL, " +
"deadline DATETIME NULL, " +
"xp INT NULL, " +
"is_completed TINYINT(1) DEFAULT 0, " +
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"INDEX idx_username (username), " +
"INDEX idx_status (status)" +
")";Tasks (auto-skip expired assigned tasks with penalty):
public void autoSkipExpiredAssignedTasks(String username, String language, String level) {
String selectExpired = "SELECT task_name FROM user_tasks WHERE username = ? AND status = 'assigned' AND completed_at < (NOW() - INTERVAL 24 HOUR)";
String updateSQL = "UPDATE user_tasks SET status='skipped', xp_earned=?, time_taken=?, completed_at=? WHERE username=? AND task_name=? AND status='assigned'";
try (Connection conn = dbHelper.getConnection();
PreparedStatement sel = conn.prepareStatement(selectExpired);
PreparedStatement upd = conn.prepareStatement(updateSQL)) {
sel.setString(1, username);
ResultSet rs = sel.executeQuery();
while (rs.next()) {
String taskName = rs.getString("task_name");
int reward = getXpRewardForTaskName(taskName, language, level);
int penalty = -(Math.max(1, reward / 2));
upd.setInt(1, penalty);
upd.setInt(2, 1440); // 24h in minutes
upd.setTimestamp(3, Timestamp.valueOf(java.time.LocalDateTime.now()));
upd.setString(4, username);
upd.setString(5, taskName);
upd.executeUpdate();
// Reflect penalty to user's total XP so UI progress matches net history
try {
new com.forgegrid.service.LevelService().addXP(username, penalty);
} catch (Exception ignored) {}
}
} catch (SQLException e) {
System.err.println("Error auto-skipping expired tasks: " + e.getMessage());
e.printStackTrace();
}
}Goated task completion (credit XP):
public boolean markGoatedTaskComplete(String username, int taskId) {
String select = "SELECT xp, is_completed FROM user_tasks WHERE id = ? AND username = ? AND type = 'goated'";
String update = "UPDATE user_tasks SET is_completed = 1, status = 'completed', xp_earned = COALESCE(xp, 0), completed_at = ? WHERE id = ? AND username = ?";
try (Connection conn = dbHelper.getConnection();
PreparedStatement sel = conn.prepareStatement(select);
PreparedStatement upd = conn.prepareStatement(update)) {
sel.setInt(1, taskId);
sel.setString(2, username);
ResultSet rs = sel.executeQuery();
if (!rs.next()) return false;
if (rs.getBoolean("is_completed")) return true;
int xp = rs.getInt("xp");
upd.setTimestamp(1, java.sql.Timestamp.valueOf(java.time.LocalDateTime.now()));
upd.setInt(2, taskId);
upd.setString(3, username);
int ok = upd.executeUpdate();
if (ok > 0) {
try {
new com.forgegrid.service.LevelService().addXP(username, xp);
} catch (Exception ignored) {}
return true;
}
} catch (SQLException e) {
System.err.println("Error completing goated task: " + e.getMessage());
}
return false;
}XP/Leveling (required XP per level step):
public static int getRequiredXPForLevel(int level) {
if (level <= 1) return 0;
// XP to go from level (N-1) to level N
return (int) Math.round(100 * Math.pow(1.5, level - 2));
}XP/Leveling (add XP and update level):
public LevelUpResult addXP(String username, int xpToAdd) {
try (Connection conn = dbHelper.getConnection()) {
// Get current XP and level
String selectSQL = "SELECT total_xp, level FROM users WHERE username = ?";
int currentTotalXP = 0;
int currentLevel = 1;
try (PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
currentTotalXP = rs.getInt("total_xp");
currentLevel = rs.getInt("level");
}
}
// Add new XP
int newTotalXP = Math.max(0, currentTotalXP + xpToAdd);
int newLevel = calculateLevelFromXP(newTotalXP);
boolean leveledUp = newLevel > currentLevel;
// Update database
String updateSQL = "UPDATE users SET total_xp = ?, level = ?, updated_at = ? WHERE username = ?";
try (PreparedStatement pstmt = conn.prepareStatement(updateSQL)) {
pstmt.setInt(1, newTotalXP);
pstmt.setInt(2, newLevel);
pstmt.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
pstmt.setString(4, username);
pstmt.executeUpdate();
}Dashboard (XP bar paint logic):
xpProgressBar = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int width = getWidth();
int height = getHeight();
// Background
g2.setColor(SIDEBAR_COLOR);
g2.fillRoundRect(0, 0, width, height, 10, 10);
// Progress
if (maxXP > 0) {
int progressWidth = (int) ((width * currentXP) / maxXP);
g2.setColor(new Color(80, 200, 120));
g2.fillRoundRect(0, 0, progressWidth, height, 10, 10);
}
// XP Text inside bar
g2.setColor(Color.WHITE);
g2.setFont(new Font("Segoe UI", Font.BOLD, 12));
String xpText = "XP: " + currentXP + " / " + maxXP;
FontMetrics fm = g2.getFontMetrics();
int textX = (width - fm.stringWidth(xpText)) / 2;
int textY = (height + fm.getAscent()) / 2 - 2;
g2.drawString(xpText, textX, textY);
g2.dispose();
}
};Entry point (EDT and AuthUI):
// Run the application on the Event Dispatch Thread (EDT)
SwingUtilities.invokeLater(() -> {
AuthUI authUI = new AuthUI();
authUI.setVisible(true);
});- ui/WelcomeUI.java:
JPanel,JLabel,JButton,FlowLayout,BoxLayout,BorderLayout,BasicButtonUI. - ui/AuthUI.java:
JFrame,JPanel,JLabel,JTextField,JPasswordField,JCheckBox,JButton,CardLayout,BoxLayout,FlowLayout,BorderLayout,BasicButtonUI,SwingWorker,JOptionPane. - ui/OnboardingInAppPanel.java:
JPanel,CardLayout,JLabel,JToggleButton,ButtonGroup,JButton,BoxLayout,FlowLayout,BorderLayout,GridBagLayout,BasicButtonUI. - ui/Dashboard.java:
JFrame,JPanel,JLabel,JButton,JList,JTable,JScrollPane,JProgressBar(custom-painted/standard), layouts:BorderLayout,FlowLayout,BoxLayout, plus basic borders. All text uses black; backgrounds use defaultUIManagerpanel color. - ui/TaskPopupDialog.java:
JDialog,JPanel,JLabel,JButton,JTextArea,JScrollPane. - ui/CardContainerPanel.java:
JPanelwith simple line border; basic white background. - ui/LoadingScreen.java:
JPanel,JLabel; simple white card. - ui/Theme.java: Brand color constants.
- WelcomeUI → Start: Triggers listener →
AuthUIshowsLOGINcard. - AuthUI → Login: Placeholder normalize → disable →
AuthController.login→ on success, check onboarding via controller → showLOADING→ either show onboarding prompt or openDashboard(embedding its content pane in same frame). - AuthUI → Sign Up: Validate →
AuthController.register→ on success, show success dialog and switch toLOGIN. - Onboarding Prompt (Yes): Build
OnboardingInAppPaneland show it. - Onboarding Prompt (Skip):
AuthController.saveOnboardingData("Skipped", "Not specified", "Not specified")→ open dashboard. - OnboardingInAppPanel → Continue: Store selections (goal, language, skill) → final Continue calls listener →
OnboardingController.saveOnboardingData(username, ...)→AuthUIopens dashboard. - Dashboard actions: Start/Done/Skip wire to services via controller:
recordAssignedTask,saveCompletedTask+LevelService.addXP(+xp),saveSkippedTask+LevelService.addXP(-xp).
- Register driver & open connection:
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection(dbUrl, dbUsername, dbPassword);- Central connection getter:
public Connection getConnection() throws SQLException {
if (connection == null || connection.isClosed()) {
connection = DriverManager.getConnection(dbUrl, dbUsername, dbPassword);
}
return connection;
}- Create statement / prepared statements:
try (Connection conn = dbHelper.getConnection();
Statement stmt = conn.createStatement()) {
stmt.execute(createTableSQL);
}try (Connection conn = dbHelper.getConnection();
PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
// set params ...
}- Execute query/update examples:
pstmt.setString(1, usernameOrEmail.trim());
pstmt.setString(2, usernameOrEmail.trim());
pstmt.setString(3, hashedPassword);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) { /* build profile */ }
}- Close connection: try-with-resources auto-closes; optional explicit:
public void closeConnection() {
try {
if (connection != null && !connection.isClosed()) {
connection.close();
}
} catch (SQLException e) { }
}config/EnvironmentConfig.java: Reads.env/env, can convertRAILWAY_MYSQL_URLto JDBC.db/DatabaseHelper.java: Builds JDBC URL, registers driver, initializes tables and indexes.
- auth/AuthService.java:
INSERT users(register),SELECT users(login),UPDATE users(reset password), existence checks (COUNT(*)). - service/UserService.java:
SELECT/UPDATE usersfor onboarding;INSERT/UPSERT user_preferences; reads preferences/profile details; existence checks. - service/HardcodedTaskService.java:
CREATE TABLE user_tasks, migrations (ALTER/CREATE INDEX), task writes (INSERT completed|skipped|assigned), reads (SELECT history/aggregates),UPDATEfor auto-skip, CRUD for goated. - service/LevelService.java: Reads XP/level (
SELECT), writes updated totals (UPDATE). - db/DatabaseHelper.java:
CREATE TABLE users,CREATE TABLE user_preferences, index creation.
- app/Main.java: Starts UI on EDT.
- ui/WelcomeUI.java: Landing card; Start → login.
- ui/AuthUI.java: Manages cards: WELCOME, LOGIN, SIGNUP, ONBOARDING_PROMPT, LOADING; delegates to controllers/services; swaps in
Dashboardcontent. - ui/OnboardingInAppPanel.java: Q1/Q2/Q3 or Welcome Back; emits completion to caller.
- ui/Dashboard.java: Simplified main UI; sidebar, center cards, task actions, stats; basic Swing colors; text black.
- ui/TaskPopupDialog.java: Minimal dialog for task actions wired to services.
- ui/CardContainerPanel.java: Simple white bordered card.
- ui/LoadingScreen.java: Minimal loading view.
- ui/Theme.java: Brand colors (pink/yellow/blue/gold).
- Onboarding fields stored on
userstable drive catalogs. - Flow:
AuthUI→ (maybe)OnboardingInAppPanel→ persist viaUserService→Dashboardloads tasks usingHardcodedTaskService.getTasksForUser(language, level)selecting beginner/intermediate/advanced lists; penalties for auto-skip use same catalog to compute XP.
public List<HardcodedTask> getTasksForUser(String language, String level) { /* ... */ }public void autoSkipExpiredAssignedTasks(String username, String language, String level) { /* ... */ }ui/AuthUILogin button →AuthController.login→auth/AuthService.login(SELECT with username/email OR + hashed password).- On success,
UserService.hasCompletedOnboardingByUsernamecheck. - If new user →
OnboardingInAppPanel→UserService.saveOnboardingDataByUsername. AuthUIembedsDashboardcontent;DashboardusesHardcodedTaskService.getTasksForUserper onboarding.
- AuthUI: Login button wiring
loginButton.addActionListener(e -> handleLogin());- AuthUI: Embed Dashboard content
Container dashboardContent = dashboard.getContentPane();
setContentPane(dashboardContent);
setTitle("ForgeGrid");- AuthUI: Onboarding prompt Skip → save + open dashboard
skipBtn.addActionListener(e -> {
if (currentProfile != null) {
controller.saveOnboardingData(currentProfile.getUsername(), "Skipped", "Not specified", "Not specified");
}
});- WelcomeUI: Start button hook
public void addStartActionListener(ActionListener l) { startButton.addActionListener(l); }- DatabaseHelper: Driver registration
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection(dbUrl, dbUsername, dbPassword);- AuthService: Login WHERE clause
WHERE (username = ? OR email = ?) AND password = ?- UserService: Save onboarding by username
UPDATE users SET onboarding_completed = 1, onboarding_goal = ?,
onboarding_language = ?, onboarding_skill = ?, updated_at = ? WHERE username = ?- HardcodedTaskService: Select tasks based on onboarding
if (lvl.contains("beginner")) tasks = getBeginnerTasks(lang);
else if (lvl.contains("intermediate")) tasks = getIntermediateTasks(lang);
else if (lvl.contains("advanced") || lvl.contains("expert")) tasks = getAdvancedTasks(lang);- HardcodedTaskService: Auto-skip penalty outline
int reward = getXpRewardForTaskName(taskName, language, level);
int penalty = -(Math.max(1, reward / 2));
upd.setInt(1, penalty);- LevelService: Update XP/level
String updateSQL = "UPDATE users SET total_xp = ?, level = ?, updated_at = ? WHERE username = ?";
pstmt.executeUpdate();- Dashboard: Start Next Task button wiring
JButton startTaskBtn = new JButton("Start Next Task");
startTaskBtn.setBackground(Theme.BRAND_PINK);
startTaskBtn.addActionListener(e -> showTaskPopup());-
Purpose: Move logic out of UI (Swing) into a thin controller layer that delegates to services and preferences. The facelift kept controllers unchanged; only UI styling/layout was simplified.
-
AuthController (
controller/AuthController.java)- Responsibilities: registration, login, password reset, onboarding checks/saves, remember‑me.
- Used by:
ui/AuthUI.javafor all auth/onboarding actions. - Key APIs:
public PlayerProfile login(String u, String p) { return authService.login(u, p); }
public boolean register(String n, String e, String p) { return authService.register(n, e, p); }
public boolean hasCompletedOnboarding(String u) { return userService.hasCompletedOnboardingByUsername(u); }
public boolean saveOnboardingData(String u, String g, String l, String s) { return userService.saveOnboardingDataByUsername(u, g, l, s); }- OnboardingController (
controller/OnboardingController.java)- Responsibilities: read/write onboarding fields by username.
- Used by:
ui/AuthUI.java,ui/OnboardingInAppPanel.java. - Key APIs:
public String[] getOnboardingData(String username) { return userService.getOnboardingDataByUsername(username); }
public boolean saveOnboardingData(String u, String g, String l, String s) { return userService.saveOnboardingDataByUsername(u, g, l, s); }- DashboardController (
controller/DashboardController.java)- Responsibilities: glue for tasks and levels; fetch lists/stats, record assigned/completed/skipped, Goated CRUD.
- Used by:
ui/Dashboard.java,ui/TaskPopupDialog.java. - Key APIs:
public LevelService.LevelInfo getLevelInfo(String username)
public List<TaskHistoryEntry> getTaskHistory(String username, int limit)
public List<HardcodedTask> getTasksFor(String language, String level)
public void recordAssignedTask(String username, String taskName)
public boolean saveCompletedTask(String username, String taskName, int timeTaken, int xpEarned)- Facelift alignment notes:
- UI files only layout Swing and wire listeners; they no longer reach directly into services or DB.
- Controllers encapsulate all business operations; the simplified visuals (black text, default backgrounds, plain buttons) do not affect controller contracts.
- Dashboard actions (Start/Done/Skip/Goated) invoke controller methods; tables/labels are populated from controller data.
- Invalid login (wrong username/password)
- Flow: AuthUI → controller.login returns null → show error dialog → stay on LOGIN.
- Expected: No DB writes; no controller state changes; fields remain.
- Snippet:
JOptionPane.showMessageDialog(this, "Invalid username or password.", "Login Failed", JOptionPane.ERROR_MESSAGE);
showCard("LOGIN");- Login error (DB/connection issue)
- Flow: Exception thrown in login path → show error dialog → remain on LOGIN.
- Snippet:
JOptionPane.showMessageDialog(this, "Authentication error: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
showCard("LOGIN");- Valid login + onboarding already completed
- Flow: controller.login → profile;
hasCompletedOnboarding=true→ show LOADING → show welcome back → embed Dashboard. - Snippet:
- Flow: controller.login → profile;
boolean hasCompletedOnboarding = controller.hasCompletedOnboarding(profile.getUsername());
if (hasCompletedOnboarding) { createWelcomeBackOnboarding(profile.getUsername()); }
else { showCard("ONBOARDING_PROMPT"); }-
Valid login + new user (not onboarded)
- Flow: controller.login → profile;
hasCompletedOnboarding=false→ showONBOARDING_PROMPT. - Expected: No Dashboard until onboarding is saved or skipped.
- Flow: controller.login → profile;
-
Onboarding: choose Yes (complete onboarding)
- Flow: Build
OnboardingInAppPanel→ after Q1/Q2/Q3 →OnboardingController.saveOnboardingData→ open Dashboard.
- Flow: Build
-
Onboarding: Skip
- Flow: Save “Skipped/Not specified/Not specified” for user → open Dashboard.
- Snippet:
controller.saveOnboardingData(currentProfile.getUsername(), "Skipped", "Not specified", "Not specified");
openDashboardInCard(goal, language, skill);- Signup validation: empty fields
- Flow: Any field empty/placeholder → error dialog → remain on SIGNUP.
- Snippet:
JOptionPane.showMessageDialog(this, "Please fill in all fields: " + sb.toString() + ".", "Error", JOptionPane.ERROR_MESSAGE);- Signup validation: username equals email
- Flow: Show error dialog; no registration attempt.
- Snippet:
"Username and Email cannot be the same.\nPlease use different values."- Signup success
- Flow: controller.register returns true → success dialog → switch to LOGIN; placeholders reset.
- Snippet:
JOptionPane.showMessageDialog(this, "Account created successfully!", "Success", JOptionPane.INFORMATION_MESSAGE);
showLogin();- Dashboard: Start Next Task
- Flow: Click button → controller.recordAssignedTask(username, taskName) → open task popup.
- Snippet:
JButton startTaskBtn = new JButton("Start Next Task");
startTaskBtn.addActionListener(e -> showTaskPopup());-
Task completion
- Flow: Submit in dialog → controller.saveCompletedTask(...) → LevelService.addXP(+xp) internally via service → UI refresh.
-
Task skip
- Flow: Skip in dialog → controller.saveSkippedTask(...) (via service) → LevelService.addXP(-xp) → UI refresh.
-
Auto-skip expired assigned tasks
- Flow: On dashboard refresh/tick → controller.autoSkipExpired(username, language, level) → converts to skipped and applies penalty.
- Snippet:
int reward = getXpRewardForTaskName(taskName, language, level);
int penalty = -(Math.max(1, reward / 2));- Select tasks by onboarding (language + level)
if (lvl.contains("beginner")) tasks = getBeginnerTasks(lang);
else if (lvl.contains("intermediate")) tasks = getIntermediateTasks(lang);
else if (lvl.contains("advanced") || lvl.contains("expert")) tasks = getAdvancedTasks(lang);- Record a task as assigned (first time only)
String existsSQL = "SELECT 1 FROM user_tasks WHERE username = ? AND task_name = ? LIMIT 1";
String insertSQL = "INSERT INTO user_tasks (username, task_name, time_taken, xp_earned, status, completed_at) VALUES (?, ?, NULL, 0, 'assigned', ?)";- Save completed task
String insertSQL =
"INSERT INTO user_tasks (username, task_name, time_taken, xp_earned, status, completed_at) " +
"VALUES (?, ?, ?, ?, 'completed', ?)";- Save skipped task (negative XP)
String insertSQL =
"INSERT INTO user_tasks (username, task_name, time_taken, xp_earned, status, completed_at) " +
"VALUES (?, ?, ?, ?, 'skipped', ?)";- Auto‑skip expired assigned tasks (24h) with 50% penalty
String selectExpired = "SELECT task_name FROM user_tasks WHERE username = ? AND status = 'assigned' AND completed_at < (NOW() - INTERVAL 24 HOUR)";
String updateSQL = "UPDATE user_tasks SET status='skipped', xp_earned=?, time_taken=?, completed_at=? WHERE username=? AND task_name=? AND status='assigned'";int reward = getXpRewardForTaskName(taskName, language, level);
int penalty = -(Math.max(1, reward / 2));
upd.setInt(1, penalty);- History and aggregates
String selectSQL =
"SELECT task_name, time_taken, xp_earned, status, completed_at " +
"FROM user_tasks WHERE username = ? ORDER BY completed_at DESC LIMIT ?";String selectSQL = "SELECT COALESCE(SUM(xp_earned), 0) as total FROM user_tasks WHERE username = ?";- Goated tasks – create example
String sql = "INSERT INTO user_tasks (username, task_name, title, description, deadline, xp, status, type, is_completed, created_at) VALUES (?, ?, ?, ?, ?, ?, 'assigned', 'goated', 0, ?)";- After completing a regular task (UI applies XP)
com.forgegrid.service.LevelService.LevelUpResult result = new com.forgegrid.service.LevelService().addXP(profile.getUsername(), task.getXpReward());- After skipping a task (UI applies XP penalty)
new com.forgegrid.service.LevelService().addXP(profile.getUsername(), xpPenalty);- Auto‑skip expired (service applies penalty)
new com.forgegrid.service.LevelService().addXP(username, penalty);- Goated task completion (service credits XP)
new com.forgegrid.service.LevelService().addXP(username, xp);- Problem: Settings section had dark backgrounds making black text unreadable
- Solution: Removed dark backgrounds from setting cards and account panels, kept black text for readability
- Enhancement: Added rose gradient backgrounds to profile stat cards with white text for contrast
- Layout: Improved profile section with horizontal layout using
BorderLayout - Result: Clean, readable settings interface with consistent pink theme
- Problem: Home page stat cards had pink gradient backgrounds that didn't match settings page style
- Solution: Changed card backgrounds from pink gradient to
PANEL_COLOR(settings-style gray background) with pink borders - Enhancement: Updated Profile view layout to center Account Information panel using horizontal
BorderLayout - Result: Consistent styling across home page and settings with improved visual balance