");
+
+ sb.append("
ClinX02 Seizure Report
");
+ sb.append("
Calendar timeline overview and detailed event log
");
+
+ // Summary box
+ sb.append("
");
+ sb.append("
");
+
+ sb.append("
");
+ sb.append("
Calendar range
");
+ sb.append("
").append(MONTHS_TO_SHOW).append(" months
");
+ sb.append("
");
+
+ sb.append("
");
+ sb.append("
Total seizure events
");
+ sb.append("
").append(groups.size()).append("
");
+ sb.append("
");
+
+ sb.append("
");
+ sb.append("
Days with seizures
");
+ sb.append("
").append(byDay.size()).append("
");
+ sb.append("
");
+
+ sb.append("
");
+ sb.append("
Download Report");
+ sb.append("
");
+
+ // Legend
+ sb.append("
");
+ sb.append("
");
+ sb.append("
Morning background (06-12)
");
+ sb.append("
Afternoon background (12-18)
");
+ sb.append("
Evening background (18-24)
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("
");
+
+ // Month selector
+ sb.append("
");
+ sb.append("");
+ sb.append("");
+ sb.append("");
+ sb.append("
");
+
+ // Monthly time-of-day summary chart
+ for (Calendar reportMonth : reportMonths) {
+ String monthId = monthIdFormat.format(reportMonth.getTime());
+ boolean activeMonth = monthId.equals(defaultMonthId);
+ int nightCount = 0;
+ int morningCount = 0;
+ int afternoonCount = 0;
+ int eveningCount = 0;
+
+ for (SeizureGroup g : groups) {
+ if (g.startDate != null && monthId.equals(monthIdFormat.format(g.startDate))) {
+ Calendar eventCal = Calendar.getInstance();
+ eventCal.setTime(g.startDate);
+ int hour = eventCal.get(Calendar.HOUR_OF_DAY);
+ if (hour < 6) {
+ nightCount++;
+ } else if (hour < 12) {
+ morningCount++;
+ } else if (hour < 18) {
+ afternoonCount++;
+ } else {
+ eveningCount++;
+ }
+ }
+ }
+
+ int maxTimeCount = Math.max(Math.max(nightCount, morningCount), Math.max(afternoonCount, eveningCount));
+ if (maxTimeCount < 1) maxTimeCount = 1;
+
+ sb.append("
");
+ sb.append("
Seizures by time of day
");
+ appendSummaryBar(sb, "Night", "00-06", "night", nightCount, maxTimeCount);
+ appendSummaryBar(sb, "Morning", "06-12", "morning", morningCount, maxTimeCount);
+ appendSummaryBar(sb, "Afternoon", "12-18", "afternoon", afternoonCount, maxTimeCount);
+ appendSummaryBar(sb, "Evening", "18-24", "evening", eveningCount, maxTimeCount);
+ sb.append("
");
+ }
+
+ // Calendar months
+ for (Calendar reportMonth : reportMonths) {
+ String monthId = monthIdFormat.format(reportMonth.getTime());
+ boolean activeMonth = monthId.equals(defaultMonthId);
+
+ Calendar cal = (Calendar) reportMonth.clone();
+ int currentMonth = cal.get(Calendar.MONTH);
+ int currentYear = cal.get(Calendar.YEAR);
+ int daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
+ int firstDayOfWeek = cal.get(Calendar.DAY_OF_WEEK); // 1=Sun, 2=Mon...
+
+ sb.append("
");
+ sb.append("
").append(monthFormat.format(reportMonth.getTime())).append("
");
+ sb.append("
");
+
+ String[] dayNames = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
+ for (String d : dayNames) {
+ sb.append("");
+ }
+
+ for (int i = 1; i < firstDayOfWeek; i++) {
+ sb.append("
");
+ }
+
+ Calendar today2 = Calendar.getInstance();
+ for (int day = 1; day <= daysInMonth; day++) {
+ String dayKey = String.format(Locale.UK, "%04d-%02d-%02d", currentYear, currentMonth + 1, day);
+ ArrayList
dayGroups = byDay.get(dayKey);
+ boolean isToday = (day == today2.get(Calendar.DAY_OF_MONTH)
+ && currentMonth == today2.get(Calendar.MONTH)
+ && currentYear == today2.get(Calendar.YEAR));
+
+ String boxClass = "day-box";
+ if (isToday) boxClass += " today";
+ if (dayGroups != null && !dayGroups.isEmpty()) boxClass += " has-seizures";
+
+ sb.append("");
+ sb.append("
").append(day).append("
");
+
+ if (dayGroups != null && !dayGroups.isEmpty()) {
+ sb.append("
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("");
+ sb.append("");
+ sb.append("");
+ sb.append("
");
+
+ for (SeizureGroup g : dayGroups) {
+ int startMinutes = getMinutesOfDay(g.startDate);
+ int durationMinutes = parseDurationMinutes(g.durationStr);
+
+ double leftPct = (startMinutes / 1440.0) * 100.0;
+ double widthPct = Math.max((durationMinutes / 1440.0) * 100.0, 1.4);
+
+ if (leftPct + widthPct > 100.0) {
+ widthPct = Math.max(0.8, 100.0 - leftPct);
+ }
+
+ String timeStr = g.startDate != null ? timeFormat.format(g.startDate) : "";
+ String label = getTimeOfDayLabel(g.startDate);
+ String tip = label + " " + timeStr + " - " + statusToString(g.firstEvent.status)
+ + " (" + g.durationStr + ")";
+
+ sb.append("
");
+ }
+
+ sb.append("
");
+ sb.append("
0006121824
");
+ sb.append("
").append(dayGroups.size())
+ .append(dayGroups.size() == 1 ? " event" : " events").append("
");
+ }
+
+ sb.append("
");
+ }
+
+ sb.append(""); // calendar
+ sb.append("
"); // month-panel
+ }
+
+ // Detailed table below. JavaScript filters this table to the selected month.
+ sb.append("
Event Details
");
+ if (groups.isEmpty()) {
+ sb.append("
No seizure events recorded in this period.
");
+ } else {
+ sb.append("
No seizure events recorded for the selected month.
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("");
+ sb.append("
");
+ sb.append("
");
+ sb.append("
");
+ sb.append("| # | Date & Time | Status | Duration | Heart Rate | Cause |
");
+ SimpleDateFormat displaySdf = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.UK);
+ int i = 1;
+ for (SeizureGroup g : groups) {
+ String timeStr = g.startDate != null ? displaySdf.format(g.startDate) : g.firstEvent.dataTime;
+ String statusStr = statusToString(g.firstEvent.status);
+ String cssClass = statusToCssClass(g.firstEvent.status);
+ String hrStr = g.hr > 0 ? String.format(Locale.UK, "%.1f bpm", g.hr) : "N/A";
+ String cause = g.firstEvent.alarmCause != null && !g.firstEvent.alarmCause.isEmpty()
+ ? g.firstEvent.alarmCause : "Unknown";
+ String dotColor = getTimeOfDayColor(g.startDate);
+ String eventMonth = g.startDate != null ? monthIdFormat.format(g.startDate) : "";
+
+ sb.append("");
+ sb.append("| ").append(i++).append(" | ");
+ sb.append("").append(timeStr).append(" | ");
+ sb.append("").append(statusStr).append(" | ");
+ sb.append("").append(escapeHtml(g.durationStr)).append(" | ");
+ sb.append("").append(hrStr).append(" | ");
+ sb.append("").append(escapeHtml(cause)).append(" | ");
+ sb.append("
");
+ }
+ sb.append("
");
+ }
+
+ sb.append("");
+ sb.append("
");
+
+ sb.append("");
+ sb.append("");
+
+ return sb.toString();
+ }
+
+ static class SeizureEvent {
+ String dataTime;
+ int status;
+ String type;
+ String notes;
+ String alarmPhrase;
+ String alarmCause;
+ double hr = 0.0;
+ Date date;
+ }
+
+ static class SeizureGroup {
+ SeizureEvent firstEvent;
+ SeizureEvent lastEvent;
+ Date startDate;
+ Date lastDate;
+ String durationStr = "N/A";
+ double hr = 0.0;
+
+ SeizureGroup(SeizureEvent first) {
+ this.firstEvent = first;
+ this.lastEvent = first;
+ this.startDate = first.date;
+ this.lastDate = first.date;
+ if (first.notes != null && first.notes.contains("Duration:")) {
+ this.durationStr = extractDuration(first.notes);
+ }
+ this.hr = first.hr;
+ }
+ }
+}
diff --git a/app/src/main/java/uk/org/openseizuredetector/SdWebServer.java b/app/src/main/java/uk/org/openseizuredetector/SdWebServer.java
index d777932..5d300c7 100644
--- a/app/src/main/java/uk/org/openseizuredetector/SdWebServer.java
+++ b/app/src/main/java/uk/org/openseizuredetector/SdWebServer.java
@@ -26,11 +26,17 @@ import fi.iki.elonen.NanoHTTPD;
*/
public class SdWebServer extends NanoHTTPD {
private String TAG = "WebServer";
+ private static final String REPORT_TOKEN = "clinx02secure";
private SdData mSdData;
private SdServer mSdServer;
private Context mContext;
private Handler mHandler;
private OsdUtil mUtil;
+
+ private boolean isValidToken(Map