Goal: keep the window responsive, make shared data thread-safe, and ship a runnable v1.0.

1. A daemon thread for overdue loans
It wakes every few seconds, asks the database, and reports back on the EDT with invokeLater.
src/lms/service/OverdueChecker.java
package lms.service; import java.util.function.IntConsumer; /** * OverdueChecker - a background DAEMON thread (Week 12). * * Every few seconds it asks the database how many loans are overdue and * hands the number to a callback. It never touches Swing itself: the * callback is responsible for jumping onto the Event Dispatch Thread. * * Daemon = "do not keep the program alive just for me": when the window * closes and the main work ends, the JVM exits even though this loop * would run forever. */ public class OverdueChecker implements Runnable { private final LibraryController controller; private final IntConsumer onResult; private final long intervalMillis; public OverdueChecker(LibraryController controller, long intervalMillis, IntConsumer onResult) { this.controller = controller; this.intervalMillis = intervalMillis; this.onResult = onResult; } /** Creates, configures and starts the thread. */ public static Thread start(LibraryController controller, long intervalMillis, IntConsumer onResult) { Thread t = new Thread(new OverdueChecker(controller, intervalMillis, onResult), "overdue-checker"); t.setDaemon(true); t.start(); return t; } @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { onResult.accept(controller.countOverdue()); Thread.sleep(intervalMillis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // restore the flag and leave the loop } catch (RuntimeException e) { System.err.println("Overdue check failed: " + e.getMessage()); // keep the thread alive try { Thread.sleep(intervalMillis); } catch (InterruptedException ie) { return; } } } } }
2. A SwingWorker for the slow import
doInBackground() runs off the EDT; process() and done() run on it. That is why the progress bar can be updated safely.
src/lms/ui/ImportWorker.java
package lms.ui; import lms.service.LibraryController; import javax.swing.*; import java.util.List; /** * ImportWorker - a SwingWorker (Week 12). * * The rule of Swing threading: * - SLOW work must NOT run on the Event Dispatch Thread (the window freezes). * - Swing components may ONLY be touched ON the Event Dispatch Thread. * * SwingWorker gives us both: * doInBackground() runs on a worker thread -> slow work goes here * publish()/process()/done() run on the EDT -> safe to update the GUI */ public class ImportWorker extends SwingWorker<Integer, Integer> { private static final String[][] SAMPLE = { {"Design Patterns", "Erich Gamma"}, {"The Pragmatic Programmer", "Andrew Hunt"}, {"Code Complete", "Steve McConnell"}, {"Java Concurrency in Practice", "Brian Goetz"}, {"Domain-Driven Design", "Eric Evans"}, {"Clean Architecture", "Robert C. Martin"}, {"Head First Design Patterns", "Eric Freeman"}, {"Algorithms", "Robert Sedgewick"}, {"Operating System Concepts", "Abraham Silberschatz"}, {"Database System Concepts", "Abraham Silberschatz"}, }; private final LibraryController controller; private final JProgressBar bar; private final Runnable whenFinished; public ImportWorker(LibraryController controller, JProgressBar bar, Runnable whenFinished) { this.controller = controller; this.bar = bar; this.whenFinished = whenFinished; } @Override protected Integer doInBackground() throws Exception { // WORKER thread int added = 0; for (int i = 0; i < SAMPLE.length; i++) { String id = "I" + String.format("%03d", i + 1); try { controller.addItem("Book", id, SAMPLE[i][0], SAMPLE[i][1]); added++; } catch (IllegalArgumentException alreadyThere) { // imported before - skip it } Thread.sleep(400); // pretend this is slow (network / big file) publish((i + 1) * 100 / SAMPLE.length); } return added; } @Override protected void process(List<Integer> chunks) { // EDT bar.setValue(chunks.get(chunks.size() - 1)); } @Override protected void done() { // EDT try { int added = get(); bar.setString("Imported " + added + " book(s)"); } catch (Exception e) { bar.setString("Import failed"); } whenFinished.run(); } }
3. synchronized where threads meet
The table refresh (EDT) and the import (worker) both use Library. All of Library's methods are now synchronized, and so are the controller's compound operations. See check-then-act in addItem.
src/lms/service/Library.java
package lms.service; import lms.exception.ItemNotAvailableException; import lms.exception.MemberNotFoundException; import lms.model.Book; import lms.model.DVD; import lms.model.LibraryItem; import lms.model.Loanable; import lms.model.Member; import lms.model.Reservable; import java.util.*; import java.util.stream.Collectors; /** * Library - Week 12 version: every method that touches the shared data is * SYNCHRONIZED, because the GUI thread and background threads now share it. * (Week 10: Generics, Collections, Streams.) * * Kept from Week 3: printAllLateFees(), the addItem() overloads. * New this week: methods that talk to INTERFACE types (Loanable, * Reservable) instead of concrete classes, and a Comparable-based sort. */ public class Library { // Week 10: Lists replaced by generic Repositories (Maps inside) private final Repository<LibraryItem> items = new Repository<>(LibraryItem::getId); private final Repository<Member> members = new Repository<>(Member::getMemberId); // ---- from Week 3 (unchanged) ---- public synchronized void addItem(LibraryItem item) { items.add(item); } public synchronized void addItem(Book book) { items.add(book); } public synchronized void addItem(DVD dvd) { items.add(dvd); } public synchronized void addMember(Member member) { members.add(member); } /** True if an item with this id already exists (used to reject duplicates). */ public synchronized boolean hasItem(String itemId) { return items.exists(itemId); } public synchronized void printAllLateFees(int daysLate) { System.out.println("--- Late fees if " + daysLate + " day(s) late ---"); for (LibraryItem item : items.findAll()) { System.out.println(item.getTitle() + ": " + item.calculateLateFee(daysLate)); } } // ---- NEW in Week 4 ---- /** * The parameter type is the INTERFACE Loanable, not LibraryItem. * This method would work for anything that promises to be loanable, * even a class that is not a LibraryItem at all. */ public synchronized void lend(Loanable thing, Member member) throws ItemNotAvailableException { thing.loan(member); } // ---- NEW in Week 5 ---- /** Looks a member up by id. Throws the UNCHECKED MemberNotFoundException. */ public synchronized Member findMember(String memberId) { return members.findById(memberId).orElseThrow(() -> new MemberNotFoundException(memberId)); } /** Looks an item up by id. Throws the CHECKED ItemNotAvailableException. */ public synchronized LibraryItem findItem(String itemId) throws ItemNotAvailableException { Optional<LibraryItem> found = items.findById(itemId); if (found.isEmpty()) throw new ItemNotAvailableException("No item with id '" + itemId + "'"); return found.get(); } /** The one-stop "borrow" operation the GUI will call later. */ public synchronized void borrowItem(String itemId, String memberId) throws ItemNotAvailableException { Member member = findMember(memberId); LibraryItem item = findItem(itemId); item.loan(member); } public synchronized void returnItem(String itemId) throws ItemNotAvailableException { findItem(itemId).returnItem(); } /** * Only some items are Reservable. Checking for the INTERFACE (not for * Book) means a future Reservable class works here with no changes. */ public void reserveIfPossible(LibraryItem item, Member member) { if (item instanceof Reservable r) { r.reserve(member); } else { System.out.println(item.getTitle() + " cannot be reserved."); } } /** Prints members ranked by activity, using Member's compareTo(). */ public synchronized void printMembersByActivity() { List<Member> sorted = members.findAll(); Collections.sort(sorted); System.out.println("--- Members ranked by activity ---"); for (Member m : sorted) { System.out.println(m.getName() + " (" + m.getBorrowedCount() + " borrowed)"); } } public synchronized void listItems() { System.out.println("--- Items in the library ---"); for (LibraryItem item : items.findAll()) System.out.println(item); } public synchronized void listMembers() { System.out.println("--- Registered members ---"); for (Member m : members.findAll()) System.out.println(m); } public synchronized List<LibraryItem> getItems() { return items.findAll(); } public synchronized List<Member> getMembers() { return members.findAll(); } // ---- NEW in Week 10: streams ---- /** Case-insensitive search in title, id, or author (for books). */ public synchronized List<LibraryItem> search(String text) { String q = text.trim().toLowerCase(); return items.search(i -> i.getTitle().toLowerCase().contains(q) || i.getId().toLowerCase().contains(q) || (i instanceof Book b && b.getAuthor().toLowerCase().contains(q))); } /** Map: member name -> number of items currently on loan. */ public synchronized Map<String, Long> loansPerMember() { return items.findAll().stream() .filter(LibraryItem::isBorrowed) .collect(Collectors.groupingBy(i -> i.getBorrower().getName(), TreeMap::new, Collectors.counting())); } /** All items that are currently available, sorted by title. */ public synchronized List<LibraryItem> availableItems() { return items.findAll().stream() .filter(i -> !i.isBorrowed()) .sorted(Comparator.comparing(LibraryItem::getTitle)) .toList(); } }
src/lms/ui/MainFrame.javaStart the checker; Import button wiring
package lms.ui; import lms.exception.DataAccessException; import lms.exception.ItemNotAvailableException; import lms.exception.MemberNotFoundException; import lms.model.*; import lms.service.LibraryController; import lms.service.OverdueChecker; import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; /** * MainFrame - Week 12: adds a background import (SwingWorker + progress bar) and a * daemon OverdueChecker thread. (Week 11: New Member + Loan History + database.) * (Week 10: search + report. Week 9: events, dialogs and MVC.) * * Same layout as Week 8. New this week: * - ActionListeners on the buttons (event-driven programming) * - JOptionPane messages for errors * - the frame talks to a LibraryController, never to the model directly */ public class MainFrame extends JFrame { private final LibraryController controller; private final DefaultTableModel tableModel; private final JTable table; private final JTextField idField = new JTextField(12); private final JTextField titleField = new JTextField(12); private final JTextField detailField = new JTextField(12); private final JComboBox<String> typeBox = new JComboBox<>(new String[]{"Book", "DVD", "Magazine"}); private final JComboBox<String> memberBox = new JComboBox<>(); private final JButton addButton = new JButton("Add Item"); private final JButton borrowButton = new JButton("Borrow"); private final JButton returnButton = new JButton("Return"); private final JButton reportButton = new JButton("Loan Report"); private final JButton historyButton = new JButton("Loan History"); private final JButton importButton = new JButton("Import Sample Books"); private final JProgressBar progressBar = new JProgressBar(0, 100); private final JLabel overdueLabel = new JLabel("Overdue: checking..."); private final JButton newMemberButton = new JButton("New Member..."); private final JTextField searchField = new JTextField(16); private final JButton searchButton = new JButton("Search"); private final JLabel statusLabel = new JLabel(" Ready"); public MainFrame(LibraryController controller) { super("Library Management System"); this.controller = controller; // ---- table (CENTER) ---- String[] columns = {"ID", "Title", "Type", "Detail", "Status"}; tableModel = new DefaultTableModel(columns, 0) { @Override public boolean isCellEditable(int row, int col) { return false; } }; table = new JTable(tableModel); table.setRowHeight(24); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.getColumnModel().getColumn(1).setPreferredWidth(200); table.getColumnModel().getColumn(3).setPreferredWidth(140); table.getColumnModel().getColumn(4).setPreferredWidth(190); // ---- form (WEST) ---- JPanel form = new JPanel(new GridLayout(0, 2, 6, 8)); form.setBorder(BorderFactory.createTitledBorder("New item")); form.add(new JLabel("Type:")); form.add(typeBox); form.add(new JLabel("ID:")); form.add(idField); form.add(new JLabel("Title:")); form.add(titleField); form.add(new JLabel("Detail:")); form.add(detailField); form.add(new JLabel("")); form.add(addButton); JPanel west = new JPanel(new BorderLayout()); west.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 4)); JPanel tools = new JPanel(new GridLayout(0, 1, 6, 6)); tools.setBorder(BorderFactory.createTitledBorder("Library")); tools.add(newMemberButton); tools.add(reportButton); tools.add(historyButton); tools.add(importButton); JPanel toolsHolder = new JPanel(new BorderLayout()); toolsHolder.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0)); toolsHolder.add(tools, BorderLayout.NORTH); west.add(form, BorderLayout.NORTH); west.add(toolsHolder, BorderLayout.CENTER); // ---- title (NORTH) ---- JLabel heading = new JLabel("Library Management System", SwingConstants.CENTER); heading.setFont(heading.getFont().deriveFont(Font.BOLD, 20f)); heading.setBorder(BorderFactory.createEmptyBorder(10, 0, 6, 0)); JPanel searchBar = new JPanel(new FlowLayout(FlowLayout.CENTER, 8, 2)); searchBar.add(new JLabel("Search:")); searchBar.add(searchField); searchBar.add(searchButton); JPanel north = new JPanel(new BorderLayout()); north.add(heading, BorderLayout.NORTH); north.add(searchBar, BorderLayout.CENTER); // ---- buttons + member picker + status (SOUTH) ---- JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 6)); buttons.add(new JLabel("Member:")); buttons.add(memberBox); buttons.add(borrowButton); buttons.add(returnButton); JPanel south = new JPanel(new BorderLayout()); south.add(statusLabel, BorderLayout.WEST); progressBar.setStringPainted(true); progressBar.setVisible(false); JPanel middle = new JPanel(new FlowLayout(FlowLayout.CENTER, 12, 6)); middle.add(overdueLabel); middle.add(progressBar); south.add(middle, BorderLayout.CENTER); south.add(buttons, BorderLayout.EAST); setLayout(new BorderLayout()); add(north, BorderLayout.NORTH); add(west, BorderLayout.WEST); add(new JScrollPane(table), BorderLayout.CENTER); add(south, BorderLayout.SOUTH); wireEvents(); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(1020, 480); setLocationRelativeTo(null); loadMembers(); refreshTable(); // Daemon thread: results arrive on ITS thread, so hop onto the EDT to touch the label. OverdueChecker.start(controller, 3000, count -> SwingUtilities.invokeLater(() -> overdueLabel.setText( "Overdue (>" + LibraryController.loanDays() + " days): " + count))); } // ------------------------------------------------------------------ // EVENTS: "when this happens, run that" // ------------------------------------------------------------------ private void wireEvents() { addButton.addActionListener(e -> onAdd()); borrowButton.addActionListener(e -> onBorrow()); returnButton.addActionListener(e -> onReturn()); searchButton.addActionListener(e -> refreshTable()); searchField.addActionListener(e -> refreshTable()); // Enter key reportButton.addActionListener(e -> JOptionPane.showMessageDialog(this, controller.loanReport(), "Loan report", JOptionPane.INFORMATION_MESSAGE)); historyButton.addActionListener(e -> onHistory()); newMemberButton.addActionListener(e -> onNewMember()); importButton.addActionListener(e -> onImport()); } private void onAdd() { try { controller.addItem((String) typeBox.getSelectedItem(), idField.getText().trim(), titleField.getText(), detailField.getText()); setStatus("Added " + titleField.getText().trim()); idField.setText(""); titleField.setText(""); detailField.setText(""); refreshTable(); } catch (IllegalArgumentException | DataAccessException ex) { showError("Cannot add item", ex.getMessage()); } } private void onBorrow() { String itemId = selectedItemId(); if (itemId == null) return; String memberId = ((String) memberBox.getSelectedItem()).split(" - ")[0]; try { controller.borrow(itemId, memberId); setStatus(itemId + " borrowed by " + memberId); refreshTable(); } catch (ItemNotAvailableException ex) { showError("Cannot borrow", ex.getMessage()); } catch (MemberNotFoundException ex) { showError("Unknown member", ex.getMessage()); } catch (DataAccessException ex) { showError("Database problem", ex.getMessage()); } } private void onReturn() { String itemId = selectedItemId(); if (itemId == null) return; try { controller.giveBack(itemId); setStatus(itemId + " returned"); refreshTable(); } catch (ItemNotAvailableException ex) { showError("Cannot return", ex.getMessage()); } catch (DataAccessException ex) { showError("Database problem", ex.getMessage()); } } private void onNewMember() { JTextField id = new JTextField(10); JTextField name = new JTextField(16); JComboBox<String> type = new JComboBox<>(new String[]{"Student", "Staff"}); JPanel panel = new JPanel(new GridLayout(0, 2, 6, 6)); panel.add(new JLabel("Member ID:")); panel.add(id); panel.add(new JLabel("Name:")); panel.add(name); panel.add(new JLabel("Type:")); panel.add(type); int choice = JOptionPane.showConfirmDialog(this, panel, "New member", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); if (choice != JOptionPane.OK_OPTION) return; try { controller.addMember((String) type.getSelectedItem(), id.getText().trim(), name.getText()); loadMembers(); setStatus("Member " + name.getText().trim() + " registered"); } catch (IllegalArgumentException | DataAccessException ex) { showError("Cannot add member", ex.getMessage()); } } private void onImport() { importButton.setEnabled(false); progressBar.setValue(0); progressBar.setString(null); progressBar.setVisible(true); new ImportWorker(controller, progressBar, () -> { importButton.setEnabled(true); refreshTable(); setStatus("Import finished"); }).execute(); // returns immediately; the GUI stays responsive } private void onHistory() { String[] cols = {"Borrowed at", "Item", "Member", "Returned at"}; DefaultTableModel model = new DefaultTableModel(cols, 0); try { for (String[] row : controller.loanHistory()) model.addRow(row); } catch (DataAccessException ex) { showError("Database problem", ex.getMessage()); return; } JScrollPane pane = new JScrollPane(new JTable(model)); pane.setPreferredSize(new Dimension(620, 180)); JOptionPane.showMessageDialog(this, pane, "Loan history", JOptionPane.PLAIN_MESSAGE); } // ------------------------------------------------------------------ // helpers // ------------------------------------------------------------------ private String selectedItemId() { int row = table.getSelectedRow(); if (row < 0) { JOptionPane.showMessageDialog(this, "Select a row in the table first.", "Nothing selected", JOptionPane.INFORMATION_MESSAGE); return null; } return (String) tableModel.getValueAt(row, 0); } private void showError(String title, String message) { JOptionPane.showMessageDialog(this, message, title, JOptionPane.WARNING_MESSAGE); setStatus(title + ": " + message); } private void setStatus(String text) { statusLabel.setText(" " + text); } private void loadMembers() { memberBox.removeAllItems(); for (Member m : controller.getMembers()) { memberBox.addItem(m.getMemberId() + " - " + m.getName()); } } private void refreshTable() { tableModel.setRowCount(0); for (LibraryItem item : controller.search(searchField.getText())) { tableModel.addRow(new Object[]{ item.getId(), item.getTitle(), item.getClass().getSimpleName(), detailOf(item), item.isBorrowed() ? "On loan: " + item.getBorrower().getName() : "Available"}); } } private String detailOf(LibraryItem item) { if (item instanceof Book b) return b.getAuthor(); if (item instanceof DVD d) return d.getDurationMinutes() + " min"; if (item instanceof Magazine m) return "Issue " + m.getIssueNumber(); return ""; } }
4. See a race condition
src/lms/RaceDemo.java
package lms; /** * RaceDemo - see a race condition with your own eyes (Week 12, console). * * Two threads each add 1 to a shared counter 200,000 times. * unsafe: count++ is THREE steps (read, add, write) - threads overwrite each other * safe: synchronized makes the three steps happen as one */ public class RaceDemo { static volatile int unsafeCount = 0; // volatile: still NOT atomic static int safeCount = 0; static synchronized void safeIncrement() { safeCount++; } public static void main(String[] args) throws InterruptedException { Runnable unsafe = () -> { for (int i = 0; i < 1_000_000; i++) unsafeCount++; }; Runnable safe = () -> { for (int i = 0; i < 1_000_000; i++) safeIncrement(); }; run(unsafe); run(safe); System.out.println("Expected : 2000000"); System.out.println("Unsynchronized: " + unsafeCount + (unsafeCount == 2000000 ? " (lucky run!)" : " <-- updates were lost")); System.out.println("Synchronized : " + safeCount); } private static void run(Runnable task) throws InterruptedException { Thread a = new Thread(task), b = new Thread(task); a.start(); b.start(); a.join(); b.join(); } }
The unsynchronised number differs on every run, and some runs even get lucky. Run it several times.
5. Run it


6. Ship v1.0
Add these files to the root of your repository:
pom.xmlMaven build with the SQLite driver and a shaded runnable jar
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>lk.ac.kln.seng12233</groupId> <artifactId>lms</artifactId> <version>1.0</version> <packaging>jar</packaging> <name>Library Management System</name> <properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> <version>3.46.0.0</version> </dependency> </dependencies> <build> <sourceDirectory>src</sourceDirectory> <plugins> <!-- Builds ONE runnable jar that includes the SQLite driver: mvn package, then java -jar target/lms-1.0-shaded.jar --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.1</version> <executions> <execution> <phase>package</phase> <goals><goal>shade</goal></goals> <configuration> <shadedArtifactAttached>true</shadedArtifactAttached> <shadedClassifierName>shaded</shadedClassifierName> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>lms.GuiMain</mainClass> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
build.shBuild without Maven
#!/usr/bin/env bash # Builds lms.jar without Maven. Needs JDK 17+ and lib/sqlite-jdbc-*.jar set -e rm -rf build && mkdir -p build/classes javac -d build/classes $(find src -name "*.java") printf 'Main-Class: lms.GuiMain\nClass-Path: lib/sqlite-jdbc.jar\n' > build/manifest.txt jar cfm build/lms.jar build/manifest.txt -C build/classes . echo "Built build/lms.jar" echo "Run with: java -jar build/lms.jar (keep a copy of the driver at build/lib/sqlite-jdbc.jar)" mkdir -p build/lib && cp lib/sqlite-jdbc-*.jar build/lib/sqlite-jdbc.jar 2>/dev/null || echo "WARNING: put sqlite-jdbc-*.jar in lib/ first"
README.mdFill in your name and index
# Library Management System (SENG 12233) A desktop Library Management System written in Java 21 with Swing and SQLite. ## Features - Catalogue of Books, DVDs and Magazines (inheritance, abstract class, interfaces) - Members: Students (3 items) and Staff (10 items) - Borrow / return with clear error messages (custom exceptions) - Search, loan report (generics + streams) - Everything is saved in `lms.db` (JDBC, DAO pattern) - Background import with a progress bar; overdue checker thread ## Run **With Maven** ```bash mvn package java -jar target/lms-1.0-shaded.jar ``` **Without Maven** (download `sqlite-jdbc-3.46.0.0.jar` into `lib/` first) ```bash ./build.sh java -jar build/lms.jar ``` ## Architecture ``` lms.model - Book, DVD, Magazine, Member, ... (MODEL) lms.ui - MainFrame, ImportWorker (VIEW) lms.service - LibraryController, Library, ... (CONTROLLER + business rules) lms.dao - Database, ItemDAO, MemberDAO, LoanDAO lms.exception- ItemNotAvailableException, MemberNotFoundException, DataAccessException ``` ## Author <your name> - <your index number>
.gitignore
out/ build/ target/ *.class lms.db *.jar !lib/.gitkeep .idea/ *.iml .vscode/
Build, run and tag:
mvn package
java -jar target/lms-1.0-shaded.jar
git tag v1.0
git push origin v1.0
Warning Test the jar on a clean copy of your repository (clone it fresh into another folder). "It works on my machine" is not a release.
Final checklist
| Area | Check |
|---|---|
| Git | All nine PRs merged, main builds, tag v1.0 on GitHub |
| MVC | View → Controller → Model/DAO; no SQL outside lms.dao |
| Errors | Every failure path shows a message; nothing crashes |
| Quality | No dead code, no System.out in the model, clear names |
| Docs | README with run instructions and screenshot; UML for each week |
| Demo | 5 to 10 minutes, rehearsed once |
Checkpoint: is it done?
- The window stays responsive while the import runs
RaceDemoshows lost updates on at least one of five runsjava -jarstarts the app from a clean clone- Tag
v1.0pushed; all PRs merged; README complete