Community Resources
diff --git a/backend/src/main/resources/static/styles.css b/backend/src/main/resources/static/styles.css
index 48b0f4d..c6b88e6 100644
--- a/backend/src/main/resources/static/styles.css
+++ b/backend/src/main/resources/static/styles.css
@@ -6,8 +6,8 @@
--success-color: #2d7d4a;
--warning-color: #d97706;
--error-color: #b91c1c;
- --bg-light: #f5f0eb;
- --bg-lighter: #faf8f5;
+ --bg-light: #f7eadc;
+ --bg-lighter: #fbecd6;
--border-color: #e5ddd5;
--text-primary: #1c1917;
--text-secondary: #6b7280;
@@ -29,7 +29,7 @@ body {
margin: 0;
padding: 0;
line-height: 1.6;
- font-size: 1rem;
+ font-size: 1.125rem;
}
/* ===== Header ===== */
@@ -64,9 +64,11 @@ body {
cursor: pointer;
}
+.logo-icon { flex-shrink: 0; }
+
.logo {
margin: 0;
- font-size: 22px;
+ font-size: 24px;
font-weight: 700;
color: var(--primary-color);
line-height: 1.2;
@@ -74,7 +76,7 @@ body {
.logo-tagline {
margin: 0;
- font-size: 12px;
+ font-size: 14px;
color: var(--text-secondary);
font-weight: 400;
}
@@ -84,9 +86,9 @@ body {
align-items: center;
gap: 8px;
background: linear-gradient(135deg, #fef3e8 0%, #fff8f0 100%);
- padding: 8px 16px;
+ padding: 9px 18px;
border-radius: 8px;
- font-size: 13px;
+ font-size: 15px;
color: var(--primary-color);
font-weight: 500;
white-space: nowrap;
@@ -101,7 +103,7 @@ body {
box-shadow: var(--shadow-sm);
}
-.ai-icon { font-size: 16px; }
+.ai-icon { font-size: 18px; }
.header-utilities {
flex: 0 0 auto;
@@ -111,16 +113,16 @@ body {
}
.utility-button {
- padding: 7px 11px;
+ padding: 8px 13px;
border: 1px solid var(--border-color);
background: white;
border-radius: 6px;
cursor: pointer;
- font-size: 13px;
+ font-size: 15px;
font-weight: 500;
color: var(--text-primary);
transition: var(--transition);
- min-width: 34px;
+ min-width: 38px;
text-align: center;
}
@@ -146,11 +148,11 @@ body {
.nav-link {
color: rgba(255,255,255,0.85);
text-decoration: none;
- padding: 11px 20px;
+ padding: 12px 22px;
display: flex;
align-items: center;
font-weight: 500;
- font-size: 14px;
+ font-size: 16px;
border-bottom: 3px solid transparent;
transition: var(--transition);
cursor: pointer;
@@ -178,14 +180,14 @@ body {
.hero-title {
margin: 0 0 10px 0;
- font-size: 30px;
+ font-size: 32px;
font-weight: 700;
line-height: 1.3;
}
.hero-subtitle {
margin: 0;
- font-size: 17px;
+ font-size: 19px;
opacity: 0.92;
line-height: 1.5;
}
@@ -199,27 +201,27 @@ body {
}
.home-main { min-width: 0; }
-.home-side { min-width: 0; }
+.home-side { min-width: 0; position: sticky; top: 160px; align-self: start; }
/* ===== Category Cards ===== */
.category-list {
display: flex;
flex-direction: column;
- gap: 10px;
+ gap: 14px;
}
.category-card {
display: flex;
align-items: center;
gap: 16px;
- padding: 18px 20px;
+ padding: 26px 24px;
background: white;
border: 1px solid var(--border-color);
border-left: 4px solid var(--accent-color);
border-radius: var(--radius);
cursor: pointer;
transition: var(--transition);
- font-size: 15px;
+ font-size: 17px;
text-align: left;
width: 100%;
}
@@ -232,25 +234,25 @@ body {
.category-card:active { transform: translateY(0); }
-.category-icon { font-size: 26px; flex-shrink: 0; width: 36px; text-align: center; }
+.category-icon { font-size: 34px; flex-shrink: 0; width: 44px; text-align: center; }
.category-card h3 {
margin: 0 0 4px 0;
- font-size: 15px;
+ font-size: 19px;
font-weight: 600;
color: var(--text-primary);
}
.category-card p {
margin: 0;
- font-size: 13px;
+ font-size: 16px;
color: var(--text-secondary);
line-height: 1.4;
}
/* ===== AI Guidance Section ===== */
.ai-guidance-section {
- background: linear-gradient(135deg, #fef3e8 0%, #fff8f0 100%);
+ background: linear-gradient(135deg, #0c5b2f 0%, #f88b0e 100%);
border: 1px solid #f5c89a;
border-radius: var(--radius);
padding: 22px;
@@ -259,25 +261,25 @@ body {
.ai-guidance-section h3 {
margin: 0 0 5px 0;
- font-size: 17px;
+ font-size: 19px;
font-weight: 600;
- color: var(--primary-color);
+ color: white;
}
.ai-guidance-subtitle {
margin: 0 0 14px 0;
- font-size: 13px;
- color: var(--text-secondary);
+ font-size: 15px;
+ color: white;
}
.ai-input-group { display: flex; gap: 8px; margin-bottom: 10px; }
.ai-input {
flex: 1;
- padding: 10px 14px;
+ padding: 11px 16px;
border: 1px solid #f5c89a;
border-radius: 8px;
- font-size: 14px;
+ font-size: 16px;
background: white;
transition: var(--transition);
}
@@ -289,23 +291,23 @@ body {
}
.ai-submit-btn {
- padding: 10px 20px;
- background: var(--accent-color);
+ padding: 11px 22px;
+ background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
- font-size: 14px;
+ font-size: 16px;
white-space: nowrap;
}
-.ai-submit-btn:hover { background: #c4662a; }
+.ai-submit-btn:hover { background: var(--primary-light); }
.ai-submit-btn:active { background: var(--primary-color); }
.ai-reset-btn {
- padding: 10px 16px;
+ padding: 11px 18px;
background: white;
color: var(--text-secondary);
border: 1px solid var(--border-color);
@@ -313,7 +315,7 @@ body {
font-weight: 500;
cursor: pointer;
transition: var(--transition);
- font-size: 14px;
+ font-size: 16px;
white-space: nowrap;
}
@@ -330,13 +332,13 @@ body {
.detail-source-link {
display: inline-block;
margin-top: 8px;
- color: white;
+ color: white !important;
background: var(--accent-color);
- padding: 7px 16px;
+ padding: 9px 18px;
border-radius: 6px;
text-decoration: none;
font-weight: 600;
- font-size: 14px;
+ font-size: 16px;
transition: var(--transition);
}
@@ -345,12 +347,12 @@ body {
.ai-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.ai-chip {
- padding: 5px 12px;
+ padding: 6px 14px;
background: white;
border: 1px solid #f5c89a;
border-radius: 20px;
cursor: pointer;
- font-size: 13px;
+ font-size: 15px;
transition: var(--transition);
}
@@ -367,9 +369,9 @@ body {
align-items: center;
gap: 12px;
background: white;
- padding: 14px 16px;
+ padding: 15px 18px;
border-radius: 8px;
- font-size: 14px;
+ font-size: 16px;
color: var(--text-secondary);
}
@@ -389,9 +391,9 @@ body {
.ai-error {
color: var(--error-color);
- font-size: 14px;
+ font-size: 16px;
background: #fef2f2;
- padding: 10px 14px;
+ padding: 12px 16px;
border-radius: 8px;
border: 1px solid #fecaca;
margin: 0;
@@ -414,11 +416,11 @@ body {
border-bottom: 1px solid var(--border-color);
}
-.ai-response-icon { font-size: 20px; flex-shrink: 0; margin-top: 1px; }
+.ai-response-icon { font-size: 22px; flex-shrink: 0; margin-top: 1px; }
.ai-response-title {
margin: 0;
- font-size: 16px;
+ font-size: 18px;
font-weight: 600;
color: var(--primary-color);
line-height: 1.3;
@@ -426,7 +428,7 @@ body {
.ai-response-notes {
margin: 0 0 12px 0;
- font-size: 13px;
+ font-size: 15px;
color: var(--text-secondary);
font-style: italic;
}
@@ -440,12 +442,12 @@ body {
}
.ai-step-number {
- width: 24px;
- height: 24px;
+ width: 26px;
+ height: 26px;
border-radius: 50%;
background: var(--accent-color);
color: white;
- font-size: 12px;
+ font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
@@ -457,21 +459,21 @@ body {
.ai-step-body { flex: 1; }
.ai-step-title {
- font-size: 14px;
+ font-size: 16px;
font-weight: 500;
color: var(--text-primary);
line-height: 1.4;
}
.ai-step-why {
- font-size: 13px;
+ font-size: 15px;
color: var(--text-secondary);
margin-top: 3px;
line-height: 1.4;
}
.ai-no-steps {
- font-size: 14px;
+ font-size: 16px;
color: var(--text-secondary);
margin: 0;
}
@@ -480,7 +482,7 @@ body {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid var(--border-color);
- font-size: 13px;
+ font-size: 15px;
color: var(--text-secondary);
}
@@ -498,13 +500,11 @@ body {
border-radius: var(--radius);
padding: 18px;
box-shadow: var(--shadow-sm);
- position: sticky;
- top: 160px;
}
.resource-panel h3 {
margin: 0 0 14px 0;
- font-size: 15px;
+ font-size: 17px;
font-weight: 600;
color: var(--text-primary);
padding-bottom: 10px;
@@ -526,13 +526,13 @@ body {
.news-item h4 {
margin: 0 0 3px 0;
- font-size: 13px;
+ font-size: 15px;
font-weight: 600;
color: var(--text-primary);
line-height: 1.3;
}
-.news-item .news-date { font-size: 11px; color: var(--text-light); }
+.news-item .news-date { font-size: 13px; color: var(--text-light); }
/* ===== Filter Screen ===== */
#filter-screen {
@@ -550,15 +550,15 @@ body {
margin-bottom: 16px;
}
-.screen-header h2 { margin: 0; font-size: 22px; font-weight: 700; }
+.screen-header h2 { margin: 0; font-size: 24px; font-weight: 700; }
.back-button {
- padding: 7px 14px;
+ padding: 8px 16px;
background: white;
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
- font-size: 14px;
+ font-size: 16px;
transition: var(--transition);
color: var(--text-primary);
white-space: nowrap;
@@ -566,17 +566,17 @@ body {
.back-button:hover { background: var(--bg-light); border-color: var(--primary-color); color: var(--primary-color); }
-.screen-subtitle { color: var(--text-secondary); font-size: 14px; margin: 0 0 18px 0; }
+.screen-subtitle { color: var(--text-secondary); font-size: 16px; margin: 0 0 18px 0; }
.filter-options { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 22px; }
.filter-chip {
- padding: 9px 16px;
+ padding: 10px 18px;
background: white;
border: 2px solid var(--border-color);
border-radius: 24px;
cursor: pointer;
- font-size: 14px;
+ font-size: 16px;
font-weight: 500;
transition: var(--transition);
color: var(--text-primary);
@@ -586,14 +586,14 @@ body {
.filter-chip.selected { background: var(--accent-color); color: white; border-color: var(--accent-color); }
.primary-button {
- padding: 11px 24px;
+ padding: 13px 26px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
- font-size: 15px;
+ font-size: 17px;
transition: var(--transition);
}
@@ -604,6 +604,20 @@ body {
.results-list { display: flex; flex-direction: column; gap: 0; }
+#news-results {
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ gap: 24px;
+ align-items: start;
+}
+
+.news-results-side {
+ min-width: 0;
+ position: sticky;
+ top: 160px;
+ align-self: start;
+}
+
/* ===== Resource Cards ===== */
.resource-card {
background: white;
@@ -634,7 +648,7 @@ body {
.card-title {
margin: 0;
- font-size: 15px;
+ font-size: 17px;
font-weight: 600;
color: var(--primary-color);
line-height: 1.3;
@@ -643,14 +657,14 @@ body {
.card-summary {
margin: 0 0 8px 0;
- font-size: 13px;
+ font-size: 15px;
color: var(--text-secondary);
line-height: 1.5;
}
.card-phone {
margin: 0 0 6px 0;
- font-size: 13px;
+ font-size: 15px;
}
.card-phone a {
@@ -663,20 +677,20 @@ body {
.card-why {
margin: 4px 0;
- font-size: 13px;
+ font-size: 15px;
color: var(--text-secondary);
line-height: 1.4;
}
.card-source {
margin: 6px 0 0 0;
- font-size: 11px;
+ font-size: 13px;
color: var(--text-light);
}
.card-cta {
margin: 8px 0 0 0;
- font-size: 12px;
+ font-size: 14px;
font-weight: 600;
color: var(--accent-color);
letter-spacing: 0.2px;
@@ -684,7 +698,7 @@ body {
.empty-state {
color: var(--text-secondary);
- font-size: 14px;
+ font-size: 16px;
padding: 20px;
text-align: center;
}
@@ -692,9 +706,9 @@ body {
/* ===== Urgency Tags ===== */
.urgency-tag {
display: inline-block;
- padding: 3px 10px;
+ padding: 4px 12px;
border-radius: 12px;
- font-size: 11px;
+ font-size: 13px;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
@@ -713,14 +727,14 @@ body {
.page-header h2 {
margin: 0 0 6px 0;
- font-size: 22px;
+ font-size: 24px;
font-weight: 700;
color: var(--primary-color);
}
.page-description {
color: var(--text-secondary);
- font-size: 14px;
+ font-size: 16px;
line-height: 1.5;
margin: 0;
}
@@ -728,7 +742,7 @@ body {
/* ===== Category Group Headers ===== */
.category-group-header {
margin: 22px 0 8px;
- font-size: 14px;
+ font-size: 16px;
font-weight: 600;
color: var(--primary-color);
text-transform: uppercase;
@@ -752,7 +766,7 @@ body {
.detail-org {
margin: 0;
- font-size: 22px;
+ font-size: 24px;
font-weight: 700;
color: var(--primary-color);
flex: 1;
@@ -767,7 +781,7 @@ body {
.detail-section:last-child { border-bottom: none; }
.detail-label {
- font-size: 11px;
+ font-size: 13px;
font-weight: 700;
color: var(--text-light);
text-transform: uppercase;
@@ -776,7 +790,7 @@ body {
}
.detail-value {
- font-size: 15px;
+ font-size: 17px;
color: var(--text-primary);
line-height: 1.5;
}
@@ -822,8 +836,8 @@ body {
}
.carousel-caption {
- padding: 10px 14px;
- font-size: 13px;
+ padding: 12px 16px;
+ font-size: 15px;
color: var(--text-secondary);
}
@@ -841,7 +855,7 @@ body {
}
.essentials-nav-label {
- font-size: 12px;
+ font-size: 14px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
@@ -851,11 +865,11 @@ body {
.essentials-nav-link {
display: inline-block;
- padding: 5px 12px;
+ padding: 6px 14px;
background: white;
border: 1px solid var(--border-color);
border-radius: 20px;
- font-size: 13px;
+ font-size: 15px;
font-weight: 500;
color: var(--primary-color);
text-decoration: none;
@@ -870,8 +884,8 @@ body {
/* ===== Results Page AI Widget ===== */
.results-ai-section {
- background: linear-gradient(135deg, #fef3e8 0%, #fff8f0 100%);
- border: 1px solid #f5c89a;
+ background: var(--accent-color);
+ border: none;
border-radius: var(--radius);
padding: 20px 22px;
margin-top: 28px;
@@ -886,15 +900,15 @@ body {
.results-ai-header h4 {
margin: 0;
- font-size: 15px;
+ font-size: 17px;
font-weight: 600;
- color: var(--primary-color);
+ color: white;
}
.results-ai-subtitle {
margin: 0 0 12px 0;
- font-size: 13px;
- color: var(--text-secondary);
+ font-size: 15px;
+ color: white;
}
.results-ai-chips {
@@ -905,12 +919,12 @@ body {
}
.results-ai-chip {
- padding: 6px 14px;
+ padding: 7px 16px;
background: white;
border: 1px solid #f5c89a;
border-radius: 20px;
cursor: pointer;
- font-size: 13px;
+ font-size: 15px;
transition: var(--transition);
color: var(--text-primary);
}
@@ -924,7 +938,7 @@ body {
margin-bottom: 10px;
}
-.results-ai-input-row .ai-input { flex: 1; }
+.results-ai-input-row .ai-input { flex: 1; border-color: rgba(255,255,255,0.6); }
/* ===== High Contrast ===== */
body.high-contrast { background-color: #000; color: #fff; }
@@ -962,17 +976,19 @@ body.high-contrast .card-cta { color: #ff0; }
.header-content { flex-direction: column; gap: 10px; padding: 12px 16px; }
.home-grid { grid-template-columns: 1fr; }
.home-side { display: none; }
- .hero-title { font-size: 24px; }
- .hero-subtitle { font-size: 15px; }
+ #news-results { grid-template-columns: 1fr; }
+ .news-results-side { display: none; }
+ .hero-title { font-size: 26px; }
+ .hero-subtitle { font-size: 17px; }
.hero-section { padding: 28px 20px; }
.ai-banner-header { display: none; }
.ai-input-group { flex-direction: column; }
.container { padding: 16px; }
.carousel-card { width: 88%; }
.nav-content { overflow-x: auto; }
- .nav-link { padding: 10px 14px; font-size: 13px; }
+ .nav-link { padding: 11px 16px; font-size: 15px; }
.card-top { flex-direction: column; gap: 6px; }
- .detail-org { font-size: 18px; }
+ .detail-org { font-size: 20px; }
.main-nav { top: 52px; }
}
diff --git a/backend/src/test/java/org/firststep/backend/controller/NewsControllerTest.java b/backend/src/test/java/org/firststep/backend/controller/NewsControllerTest.java
index debff6e..3d83a8b 100644
--- a/backend/src/test/java/org/firststep/backend/controller/NewsControllerTest.java
+++ b/backend/src/test/java/org/firststep/backend/controller/NewsControllerTest.java
@@ -49,8 +49,8 @@ RssFeedSource rssFeedSource() {
}
@Bean
- NewsService newsService(RssFeedSource rssFeedSource) {
- return new NewsService(rssFeedSource);
+ NewsService newsService() {
+ return new NewsService();
}
}
diff --git a/backend/src/test/java/org/firststep/backend/controller/ResourceControllerTest.java b/backend/src/test/java/org/firststep/backend/controller/ResourceControllerTest.java
new file mode 100644
index 0000000..c6122d8
--- /dev/null
+++ b/backend/src/test/java/org/firststep/backend/controller/ResourceControllerTest.java
@@ -0,0 +1,38 @@
+package org.firststep.backend.controller;
+
+import org.firststep.backend.service.ResourceService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@WebMvcTest(ResourceController.class)
+@ContextConfiguration(classes = {ResourceController.class, ResourceControllerTest.TestConfig.class})
+@TestPropertySource(properties = "app.seasonal.images.dir=src/test/resources/seasonal-test")
+class ResourceControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Configuration
+ static class TestConfig {
+ @Bean
+ ResourceService resourceService() {
+ return new ResourceService();
+ }
+ }
+
+ @Test
+ void shouldReturnSeasonalImagesFromConfiguredDirectory() throws Exception {
+ mockMvc.perform(get("/api/seasonal-images"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0]").value("images/seasonal/Sample.png"));
+ }
+}
diff --git a/backend/target/classes/application.properties b/backend/target/classes/application.properties
deleted file mode 100644
index 77479df..0000000
--- a/backend/target/classes/application.properties
+++ /dev/null
@@ -1,16 +0,0 @@
-server.port=8080
-spring.jackson.property-naming-strategy=LOWER_CAMEL_CASE
-
-ollama.api.url=http://localhost:11434/api/generate
-ollama.model=gemma2:2b
-
-# Feature flag to prevent blocking UI when Ollama runtime is not available
-ai.enabled=true
-
-# RSS feeds for Weekly Updates (comma-separated)
-news.rss.urls=https://winn22.org/feed,https://legis.delaware.gov/RSSFeed
-# Refresh interval in milliseconds (3600000 = 1 hour)
-news.rss.refresh-interval=3600000
-
-
-
diff --git a/backend/target/classes/org/firststep/backend/FirstStepApplication.class b/backend/target/classes/org/firststep/backend/FirstStepApplication.class
deleted file mode 100644
index ad29fe6..0000000
Binary files a/backend/target/classes/org/firststep/backend/FirstStepApplication.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/controller/DecisionController.class b/backend/target/classes/org/firststep/backend/controller/DecisionController.class
deleted file mode 100644
index 546b56e..0000000
Binary files a/backend/target/classes/org/firststep/backend/controller/DecisionController.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/controller/NewsController.class b/backend/target/classes/org/firststep/backend/controller/NewsController.class
deleted file mode 100644
index f172085..0000000
Binary files a/backend/target/classes/org/firststep/backend/controller/NewsController.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/controller/ResourceController.class b/backend/target/classes/org/firststep/backend/controller/ResourceController.class
deleted file mode 100644
index 225b2f6..0000000
Binary files a/backend/target/classes/org/firststep/backend/controller/ResourceController.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/dto/Citation.class b/backend/target/classes/org/firststep/backend/dto/Citation.class
deleted file mode 100644
index b776a1d..0000000
Binary files a/backend/target/classes/org/firststep/backend/dto/Citation.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/dto/DecisionRequest.class b/backend/target/classes/org/firststep/backend/dto/DecisionRequest.class
deleted file mode 100644
index 82dfad2..0000000
Binary files a/backend/target/classes/org/firststep/backend/dto/DecisionRequest.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/dto/DecisionResponse.class b/backend/target/classes/org/firststep/backend/dto/DecisionResponse.class
deleted file mode 100644
index 45e05d1..0000000
Binary files a/backend/target/classes/org/firststep/backend/dto/DecisionResponse.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/dto/DecisionStep.class b/backend/target/classes/org/firststep/backend/dto/DecisionStep.class
deleted file mode 100644
index cc121cb..0000000
Binary files a/backend/target/classes/org/firststep/backend/dto/DecisionStep.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/model/NewsItem.class b/backend/target/classes/org/firststep/backend/model/NewsItem.class
deleted file mode 100644
index 4a39ade..0000000
Binary files a/backend/target/classes/org/firststep/backend/model/NewsItem.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/model/Resource$Location.class b/backend/target/classes/org/firststep/backend/model/Resource$Location.class
deleted file mode 100644
index 41b235c..0000000
Binary files a/backend/target/classes/org/firststep/backend/model/Resource$Location.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/model/Resource$Phone.class b/backend/target/classes/org/firststep/backend/model/Resource$Phone.class
deleted file mode 100644
index 9166775..0000000
Binary files a/backend/target/classes/org/firststep/backend/model/Resource$Phone.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/model/Resource$Website.class b/backend/target/classes/org/firststep/backend/model/Resource$Website.class
deleted file mode 100644
index f79d9c1..0000000
Binary files a/backend/target/classes/org/firststep/backend/model/Resource$Website.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/model/Resource.class b/backend/target/classes/org/firststep/backend/model/Resource.class
deleted file mode 100644
index c064589..0000000
Binary files a/backend/target/classes/org/firststep/backend/model/Resource.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/DecisionAgentService$NewsScore.class b/backend/target/classes/org/firststep/backend/service/DecisionAgentService$NewsScore.class
deleted file mode 100644
index 443c2be..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/DecisionAgentService$NewsScore.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/DecisionAgentService$NewsServiceLike.class b/backend/target/classes/org/firststep/backend/service/DecisionAgentService$NewsServiceLike.class
deleted file mode 100644
index 67b2b27..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/DecisionAgentService$NewsServiceLike.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/DecisionAgentService$ResourceScore.class b/backend/target/classes/org/firststep/backend/service/DecisionAgentService$ResourceScore.class
deleted file mode 100644
index 00a1478..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/DecisionAgentService$ResourceScore.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/DecisionAgentService$ResourceServiceLike.class b/backend/target/classes/org/firststep/backend/service/DecisionAgentService$ResourceServiceLike.class
deleted file mode 100644
index 715f1e2..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/DecisionAgentService$ResourceServiceLike.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/DecisionAgentService.class b/backend/target/classes/org/firststep/backend/service/DecisionAgentService.class
deleted file mode 100644
index fbadbea..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/DecisionAgentService.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/NewsService$1.class b/backend/target/classes/org/firststep/backend/service/NewsService$1.class
deleted file mode 100644
index 24f0191..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/NewsService$1.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/NewsService.class b/backend/target/classes/org/firststep/backend/service/NewsService.class
deleted file mode 100644
index 9f52b01..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/NewsService.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/OllamaService.class b/backend/target/classes/org/firststep/backend/service/OllamaService.class
deleted file mode 100644
index 8fa9553..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/OllamaService.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/ResourceService$1.class b/backend/target/classes/org/firststep/backend/service/ResourceService$1.class
deleted file mode 100644
index 27912a8..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/ResourceService$1.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/ResourceService$2.class b/backend/target/classes/org/firststep/backend/service/ResourceService$2.class
deleted file mode 100644
index 0ff48c3..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/ResourceService$2.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/ResourceService$3.class b/backend/target/classes/org/firststep/backend/service/ResourceService$3.class
deleted file mode 100644
index 752498e..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/ResourceService$3.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/ResourceService.class b/backend/target/classes/org/firststep/backend/service/ResourceService.class
deleted file mode 100644
index e73e236..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/ResourceService.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/RssFeedService.class b/backend/target/classes/org/firststep/backend/service/RssFeedService.class
deleted file mode 100644
index 86efff1..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/RssFeedService.class and /dev/null differ
diff --git a/backend/target/classes/org/firststep/backend/service/RssFeedSource.class b/backend/target/classes/org/firststep/backend/service/RssFeedSource.class
deleted file mode 100644
index b40102a..0000000
Binary files a/backend/target/classes/org/firststep/backend/service/RssFeedSource.class and /dev/null differ
diff --git a/backend/target/classes/static/app.js b/backend/target/classes/static/app.js
deleted file mode 100644
index 0383c93..0000000
--- a/backend/target/classes/static/app.js
+++ /dev/null
@@ -1,848 +0,0 @@
-// ===== Translations =====
-const STRINGS = {
- en: {
- getHelp: "Get Help",
- communityInfo: "Community Info",
- announcements: "Announcements",
- communityInfoSoon: "Community Info section coming soon!",
- announcementsSoon: "Announcements section coming soon!",
- heroTitle: "What do you need help with today?",
- heroSubtitle: "Find housing, essentials, community programs and local updates in one trusted place.",
- housingHelp: "Housing Help",
- housingHelpSub: "Home and Rental Assistance, Shelters, Home Repairs",
- housingHelpDesc: "Find programs and local organizations that can help you find, buy or rent a place to live. Browse emergency shelter options, rental assistance programs and homeownership or mortgage resources. Listings include contact details and eligibility information so you can act quickly.",
- essentials: "Free / Low-Cost Essentials",
- essentialsSub: "Food, Furniture, Clothing",
- essentialsDesc: "Check out these local programs and nonprofits offering furniture, utilities, repairs for free or at a low cost. Make your home more comfortable and safe with a few simple steps.",
- seasonal: "Community Resources",
- seasonalSub: "Programs, Events and Community Opportunities",
- weeklyUpdates: "Weekly Updates",
- weeklyUpdatesSub: "News and Changes Impacting You",
- weeklyUpdatesDesc: "Stay up to date on the rules, public meetings and changes that affect housing, benefits and community services. Read the highlights and learn about important deadlines, new requirements and policy updates so you can participate and plan ahead.",
- aiTitle: "AI Guidance",
- aiSubtitle: "Ask a question in natural language to find resources",
- aiPlaceholder: "E.g., I need rental help near Wilmington for seniors",
- aiButton: "Get Help",
- aiUrgent: "🚨 Urgent",
- aiHousing: "🏠 Housing",
- aiEssentials: "🛒 Essentials",
- latestUpdates: "Latest Updates",
- refineSearch: "Refine Your Search",
- filterSubtitle: "Choose filters to narrow results.",
- viewResults: "View Results",
- backHome: "← Back to Home",
- backResults: "← Back to Results",
- back: "← Back",
- noResults: "No matching resources found.",
- loading: "Loading...",
- loadingAI: "Finding resources for you…",
- phone: "Phone",
- address: "Location",
- website: "Visit Website →",
- eligibility: "Who Qualifies",
- category: "Category",
- urgency: "Urgency",
- about: "About",
- sources: "Sources",
- viewDetails: "View Details →",
- callNow: "📞 Call",
- aiSummary: "AI Summary & Guidance",
- aiAnalyzed: "AI analyzed your query and found relevant resources",
- },
- es: {
- getHelp: "Obtener Ayuda",
- communityInfo: "Info Comunitaria",
- announcements: "Anuncios",
- communityInfoSoon: "¡La sección de Información Comunitaria estará disponible pronto!",
- announcementsSoon: "¡La sección de Anuncios estará disponible pronto!",
- heroTitle: "¿Con qué necesitas ayuda hoy?",
- heroSubtitle: "Encuentra vivienda, artículos esenciales, programas comunitarios y noticias locales en un solo lugar de confianza.",
- housingHelp: "Ayuda con Vivienda",
- housingHelpSub: "Asistencia para Alquiler, Refugios, Reparaciones del Hogar",
- housingHelpDesc: "Encuentra programas y organizaciones locales que pueden ayudarte a encontrar, comprar o alquilar un lugar donde vivir.",
- essentials: "Artículos Esenciales Gratis / Económicos",
- essentialsSub: "Comida, Muebles, Ropa",
- essentialsDesc: "Consulta estos programas locales y organizaciones sin fines de lucro que ofrecen muebles, servicios y reparaciones de forma gratuita o a bajo costo.",
- seasonal: "Recursos Comunitarios",
- seasonalSub: "Programas, Eventos y Oportunidades Comunitarias",
- weeklyUpdates: "Actualizaciones Semanales",
- weeklyUpdatesSub: "Noticias y Cambios que te Afectan",
- weeklyUpdatesDesc: "Mantente al día sobre las reglas, reuniones públicas y cambios que afectan la vivienda, los beneficios y los servicios comunitarios.",
- aiTitle: "Orientación con IA",
- aiSubtitle: "Haz una pregunta para encontrar recursos",
- aiPlaceholder: "Ej., Necesito ayuda con el alquiler cerca de Wilmington para personas mayores",
- aiButton: "Obtener Ayuda",
- aiUrgent: "🚨 Urgente",
- aiHousing: "🏠 Vivienda",
- aiEssentials: "🛒 Esenciales",
- latestUpdates: "Últimas Actualizaciones",
- refineSearch: "Refinar Búsqueda",
- filterSubtitle: "Elige filtros para reducir resultados.",
- viewResults: "Ver Resultados",
- backHome: "← Volver al Inicio",
- backResults: "← Volver a Resultados",
- back: "← Volver",
- noResults: "No se encontraron recursos.",
- loading: "Cargando...",
- loadingAI: "Buscando recursos para ti…",
- phone: "Teléfono",
- address: "Ubicación",
- website: "Visitar Sitio Web →",
- eligibility: "Quién Califica",
- category: "Categoría",
- urgency: "Urgencia",
- about: "Acerca de",
- sources: "Fuentes",
- viewDetails: "Ver Detalles →",
- callNow: "📞 Llamar",
- aiSummary: "Resumen y Orientación con IA",
- aiAnalyzed: "La IA analizó tu pregunta y encontró recursos relevantes",
- }
-};
-
-let currentLanguage = "en";
-
-function t(key) {
- return (STRINGS[currentLanguage] && STRINGS[currentLanguage][key]) || STRINGS.en[key] || key;
-}
-
-function applyLanguage() {
- document.querySelectorAll("[data-i18n]").forEach(el => {
- const key = el.getAttribute("data-i18n");
- if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
- el.placeholder = t(key);
- } else {
- el.textContent = t(key);
- }
- });
- document.getElementById("language-toggle").textContent =
- currentLanguage === "en" ? "ES" : "EN";
-}
-
-// ===== Page Navigation =====
-const homeLogoLink = document.getElementById("home-logo-link");
-const navGetHelp = document.getElementById("nav-get-help");
-const navCommunityInfo = document.getElementById("nav-community-info");
-const navAnnouncements = document.getElementById("nav-announcements");
-const backFromFilterButton = document.getElementById("back-from-filter-button");
-const backHomeButtonResults = document.getElementById("back-home-button-results");
-
-function goHome() {
- document.getElementById("home-screen").style.display = "block";
- document.getElementById("filter-screen").style.display = "none";
- document.getElementById("results-screen").style.display = "none";
- document.getElementById("detail-screen").style.display = "none";
- window.scrollTo(0, 0);
-}
-
-homeLogoLink.addEventListener("click", (e) => { e.preventDefault(); goHome(); });
-
-const aiBannerHeader = document.getElementById("ai-banner-header");
-aiBannerHeader.addEventListener("click", () => {
- goHome();
- setTimeout(() => {
- document.getElementById("ai-guidance-home").scrollIntoView({ behavior: "smooth" });
- document.getElementById("ai-question").focus();
- }, 50);
-});
-aiBannerHeader.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") aiBannerHeader.click(); });
-
-navGetHelp.addEventListener("click", (e) => {
- e.preventDefault();
- goHome();
- navGetHelp.classList.add("active");
- navCommunityInfo.classList.remove("active");
- navAnnouncements.classList.remove("active");
-});
-
-navCommunityInfo.addEventListener("click", (e) => {
- e.preventDefault();
- navGetHelp.classList.remove("active");
- navCommunityInfo.classList.add("active");
- navAnnouncements.classList.remove("active");
- showSeasonalResources();
-});
-
-navAnnouncements.addEventListener("click", (e) => {
- e.preventDefault();
- navGetHelp.classList.remove("active");
- navCommunityInfo.classList.remove("active");
- navAnnouncements.classList.add("active");
- loadNewsUpdates();
-});
-
-// ===== Language Toggle =====
-document.getElementById("language-toggle").addEventListener("click", () => {
- currentLanguage = currentLanguage === "en" ? "es" : "en";
- applyLanguage();
-});
-
-// ===== Accessibility Controls =====
-document.getElementById("contrast-button").addEventListener("click", () => {
- document.body.classList.toggle("high-contrast");
- localStorage.setItem("high-contrast", document.body.classList.contains("high-contrast"));
-});
-
-document.getElementById("increase-text-button").addEventListener("click", () => {
- const current = parseFloat(getComputedStyle(document.documentElement).fontSize);
- const next = Math.min(current + 2, 24);
- document.documentElement.style.fontSize = next + "px";
- localStorage.setItem("font-size", next + "px");
-});
-
-document.getElementById("decrease-text-button").addEventListener("click", () => {
- const current = parseFloat(getComputedStyle(document.documentElement).fontSize);
- const next = Math.max(current - 2, 12);
- document.documentElement.style.fontSize = next + "px";
- localStorage.setItem("font-size", next + "px");
-});
-
-window.addEventListener("DOMContentLoaded", () => {
- if (localStorage.getItem("high-contrast") === "true") {
- document.body.classList.add("high-contrast");
- }
- const savedFontSize = localStorage.getItem("font-size");
- if (savedFontSize) document.documentElement.style.fontSize = savedFontSize;
- loadSidebarNews();
- applyLanguage();
-});
-
-// ===== DOM References =====
-const resultsContainer = document.getElementById("results");
-const resultsScreen = document.getElementById("results-screen");
-const filterScreen = document.getElementById("filter-screen");
-const urgentFilterButton = document.getElementById("urgent-filter");
-const continueButton = document.getElementById("continue-button");
-const newsResultsContainer = document.getElementById("news-results");
-const seasonalResultsContainer = document.getElementById("seasonal-results");
-const essentialsResultsContainer = document.getElementById("essentials-results");
-const homeScreen = document.getElementById("home-screen");
-const detailScreen = document.getElementById("detail-screen");
-const detailView = document.getElementById("detail-view");
-const backResultsButton = document.getElementById("back-results-button");
-const backHomeButton = document.getElementById("back-home-button");
-
-let urgentFilterSelected = false;
-
-// ===== Category Navigation =====
-document.getElementById("housing-help-button").addEventListener("click", () => {
- homeScreen.style.display = "none";
- filterScreen.style.display = "block";
- resultsContainer.innerHTML = "";
- window.scrollTo(0, 0);
-});
-
-continueButton.addEventListener("click", () => {
- filterScreen.style.display = "none";
- loadHousingResources();
-});
-
-urgentFilterButton.addEventListener("click", () => {
- urgentFilterSelected = !urgentFilterSelected;
- urgentFilterButton.classList.toggle("selected");
-});
-
-document.getElementById("weekly-updates-button-home").addEventListener("click", loadNewsUpdates);
-document.getElementById("seasonal-resources-button").addEventListener("click", showSeasonalResources);
-document.getElementById("essentials-button").addEventListener("click", loadEssentialsResources);
-
-backHomeButton?.addEventListener("click", goHome);
-backResultsButton.addEventListener("click", hideDetailScreen);
-backFromFilterButton?.addEventListener("click", goHome);
-backHomeButtonResults?.addEventListener("click", goHome);
-
-// ===== AI Guidance =====
-const aiQuestionEl = document.getElementById("ai-question");
-const aiSubmitBtn = document.getElementById("ai-submit");
-const aiOutputEl = document.getElementById("ai-output");
-
-let aiUrgent = false;
-let aiPreferredCategories = [];
-
-function setAiChipSelected(btn, selected) {
- btn.classList.toggle("selected", selected);
-}
-
-document.getElementById("ai-urgent").addEventListener("click", () => {
- aiUrgent = !aiUrgent;
- setAiChipSelected(document.getElementById("ai-urgent"), aiUrgent);
-});
-
-document.getElementById("ai-housing").addEventListener("click", () => {
- const idx = aiPreferredCategories.indexOf("housing");
- if (idx >= 0) aiPreferredCategories.splice(idx, 1);
- else aiPreferredCategories.push("housing");
- setAiChipSelected(document.getElementById("ai-housing"), aiPreferredCategories.includes("housing"));
-});
-
-document.getElementById("ai-essentials").addEventListener("click", () => {
- const idx = aiPreferredCategories.indexOf("essentials");
- if (idx >= 0) aiPreferredCategories.splice(idx, 1);
- else aiPreferredCategories.push("essentials");
- setAiChipSelected(document.getElementById("ai-essentials"), aiPreferredCategories.includes("essentials"));
-});
-
-aiSubmitBtn.addEventListener("click", submitDecision);
-aiQuestionEl.addEventListener("keydown", (e) => { if (e.key === "Enter") submitDecision(); });
-
-document.getElementById("ai-reset").addEventListener("click", () => {
- aiQuestionEl.value = "";
- aiOutputEl.innerHTML = "";
- aiUrgent = false;
- aiPreferredCategories = [];
- setAiChipSelected(document.getElementById("ai-urgent"), false);
- setAiChipSelected(document.getElementById("ai-housing"), false);
- setAiChipSelected(document.getElementById("ai-essentials"), false);
- aiQuestionEl.focus();
-});
-
-async function submitDecision() {
- const userQuery = (aiQuestionEl.value || "").trim();
- if (!userQuery) {
- aiOutputEl.innerHTML = `${t("aiPlaceholder")}
`;
- return;
- }
-
- aiOutputEl.innerHTML = `
- `;
-
- try {
- const res = await fetch("/api/decide", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- userQuery,
- urgent: aiUrgent,
- preferredCategories: aiPreferredCategories
- })
- });
-
- if (!res.ok) {
- const text = await res.text();
- throw new Error("HTTP " + res.status + ": " + text);
- }
-
- const data = await res.json();
- renderDecisionResponse(data);
-
- } catch (err) {
- console.error(err);
- aiOutputEl.innerHTML = `Unable to get AI guidance: ${err.message || String(err)}
`;
- }
-}
-
-function renderDecisionResponse(data) {
- const title = data?.answerTitle || "Guidance";
- const notes = data?.notes || "";
- const steps = data?.steps || [];
- const citations = data?.citations || [];
-
- let stepsHtml = "";
- if (steps.length > 0) {
- stepsHtml = steps
- .sort((a, b) => (a.order || 0) - (b.order || 0))
- .map((s, i) => `
-
-
${i + 1}
-
-
${s.title || "Step"}: ${s.action || ""}
- ${s.why ? `
Why: ${s.why}
` : ""}
-
-
- `).join("");
- } else {
- stepsHtml = `${notes || "No specific steps available for this query."}
`;
- }
-
- const citationsHtml = citations.length > 0
- ? `
-
${t("sources")}:
-
${citations.map(c => `- ${c.sourceType || ""}: ${c.label || c.id || ""}
`).join("")}
-
`
- : "";
-
- aiOutputEl.innerHTML = `
-
-
- ${notes && steps.length > 0 ? `
${notes}
` : ""}
-
${stepsHtml}
- ${citationsHtml}
-
- `;
-}
-
-// ===== Resource Loading =====
-async function loadHousingResources() {
- newsResultsContainer.style.display = "none";
- essentialsResultsContainer.style.display = "none";
- seasonalResultsContainer.style.display = "none";
- detailScreen.style.display = "none";
- resultsContainer.style.display = "block";
- resultsContainer.innerHTML = `${t("loading")}
`;
-
- try {
- const response = await fetch("/api/resources");
- const resources = await response.json();
-
- let housingResources = resources.filter(r =>
- r.category && r.category.toLowerCase().includes("housing")
- );
-
- if (urgentFilterSelected) {
- housingResources = housingResources.filter(r => {
- const u = (r.urgency || "").toLowerCase();
- return u === "emergency" || u === "time-limited";
- });
- }
-
- displayResources(housingResources);
- showResultsScreen();
-
- } catch (error) {
- console.error(error);
- resultsContainer.innerHTML = "Unable to load resources.
";
- }
-}
-
-async function loadEssentialsResources() {
- resultsContainer.style.display = "none";
- newsResultsContainer.style.display = "none";
- seasonalResultsContainer.style.display = "none";
- detailScreen.style.display = "none";
- essentialsResultsContainer.style.display = "block";
- essentialsResultsContainer.innerHTML = `${t("loading")}
`;
-
- try {
- const response = await fetch("/api/resources");
- const resources = await response.json();
- const free = resources.filter(r => r.cost && r.cost.toLowerCase() === "free");
- displayEssentials(free);
- showResultsScreen();
- } catch (error) {
- console.error(error);
- essentialsResultsContainer.innerHTML = "Unable to load resources.
";
- }
-}
-
-async function loadNewsUpdates() {
- resultsContainer.style.display = "none";
- essentialsResultsContainer.style.display = "none";
- seasonalResultsContainer.style.display = "none";
- detailScreen.style.display = "none";
- newsResultsContainer.style.display = "block";
- newsResultsContainer.innerHTML = `${t("loading")}
`;
-
- try {
- const response = await fetch("/api/news");
- const newsItems = await response.json();
- displayNews(newsItems);
- showResultsScreen();
- } catch (error) {
- console.error(error);
- newsResultsContainer.innerHTML = "Unable to load updates.
";
- }
- return Promise.resolve();
-}
-
-async function loadSidebarNews() {
- try {
- const response = await fetch("/api/news");
- const items = await response.json();
- const newsResults = document.getElementById("sidebar-news");
- newsResults.innerHTML = "";
-
- items.filter(item => item.active).slice(0, 3).forEach(item => {
- const card = document.createElement("div");
- card.className = "news-item";
- card.innerHTML = `
- ${item.headline}
- ${item.published || "Latest"}
- `;
- card.addEventListener("click", () => {
- loadNewsUpdates().then(() => showNewsDetail(item));
- });
- newsResults.appendChild(card);
- });
- } catch (error) {
- console.error("Sidebar news failed to load:", error);
- document.getElementById("sidebar-news").innerHTML =
- 'Unable to load updates
';
- }
-}
-
-// ===== Screen Management =====
-let activeResultsContainer = null;
-
-function showResultsScreen() {
- homeScreen.style.display = "none";
- filterScreen.style.display = "none";
- detailScreen.style.display = "none";
- resultsScreen.style.display = "block";
- window.scrollTo(0, 0);
-}
-
-function showDetailScreen() {
- // Hide whichever results container is currently visible
- [resultsContainer, newsResultsContainer, essentialsResultsContainer, seasonalResultsContainer].forEach(el => {
- if (el.style.display !== "none") {
- activeResultsContainer = el;
- el.style.display = "none";
- }
- });
- detailScreen.style.display = "block";
- window.scrollTo(0, 0);
-}
-
-function hideDetailScreen() {
- detailScreen.style.display = "none";
- if (activeResultsContainer) {
- activeResultsContainer.style.display = "block";
- } else {
- resultsContainer.style.display = "block";
- }
- window.scrollTo(0, 0);
-}
-
-function renderPageHeader(title, description) {
- return `
-
- `;
-}
-
-function urgencyClass(urgency) {
- const u = (urgency || "standard").toLowerCase().replace(/\s+/g, "-");
- if (u === "emergency") return "urgency-emergency";
- if (u === "time-limited") return "urgency-time-limited";
- return "urgency-standard";
-}
-
-// ===== Display Functions =====
-function displayResources(resources) {
- resultsContainer.innerHTML = renderPageHeader(t("housingHelp"), t("housingHelpDesc"));
-
- if (resources.length === 0) {
- resultsContainer.innerHTML += `${t("noResults")}
`;
- return;
- }
-
- resources.forEach(resource => {
- const phone = resource.phones?.[0]?.number;
- const urgency = resource.urgency || "Standard";
-
- const card = document.createElement("div");
- card.className = "resource-card";
- card.innerHTML = `
-
-
${resource.organization}
- ${urgency}
-
- ${resource.summary || ""}
- ${phone ? `📞 ${phone}
` : ""}
- ${t("viewDetails")}
- `;
- card.addEventListener("click", () => showResourceDetails(resource));
- resultsContainer.appendChild(card);
- });
-}
-
-function displayEssentials(resources) {
- essentialsResultsContainer.innerHTML = renderPageHeader(t("essentials"), t("essentialsDesc"));
-
- if (resources.length === 0) {
- essentialsResultsContainer.innerHTML += `${t("noResults")}
`;
- return;
- }
-
- const grouped = {};
- resources.forEach(r => {
- const cat = r.category || "Other";
- if (!grouped[cat]) grouped[cat] = [];
- grouped[cat].push(r);
- });
-
- const categories = Object.keys(grouped);
-
- // Category anchor nav at the top
- const anchorSlug = cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, "-");
- const navHtml = `
-
-
Jump to:
- ${categories.map(cat => `
-
${cat}
- `).join("")}
-
- `;
- essentialsResultsContainer.innerHTML += navHtml;
-
- Object.entries(grouped).forEach(([category, items]) => {
- const slug = anchorSlug(category);
- essentialsResultsContainer.innerHTML += ``;
- items.forEach(resource => {
- const phone = resource.phones?.[0]?.number;
- const card = document.createElement("div");
- card.className = "resource-card";
- card.innerHTML = `
-
-
${resource.organization}
- Free
-
- ${resource.summary || ""}
- ${phone ? `📞 ${phone}
` : ""}
- ${t("viewDetails")}
- `;
- card.addEventListener("click", () => showResourceDetails(resource));
- essentialsResultsContainer.appendChild(card);
- });
- });
-}
-
-function displayNews(newsItems) {
- newsResultsContainer.innerHTML = renderPageHeader(t("weeklyUpdates"), t("weeklyUpdatesDesc"));
-
- newsItems.forEach(item => {
- const cats = (item.category_tags || []).join(" · ");
- const sourceLink = item.source_url
- ? `
- ${item.source_name} ↗
- `
- : item.source_name;
- const card = document.createElement("div");
- card.className = "resource-card";
- card.innerHTML = `
- ${cats || "General"}
- ${item.headline}
- ${item.summary}
- Why this matters: ${item.why_it_matters}
- ${sourceLink} · ${item.published}
- ${t("viewDetails")}
- `;
- card.addEventListener("click", () => showNewsDetail(item));
- newsResultsContainer.appendChild(card);
- });
-}
-
-function showNewsDetail(item) {
- const cats = (item.category_tags || []).join(" · ");
- const sourceLink = item.source_url
- ? `Read More →`
- : "";
-
- detailView.innerHTML = `
-
-
- ${item.body ? `
-
-
${t("about")}
-
${item.body}
-
` : item.summary ? `
-
-
${t("about")}
-
${item.summary}
-
` : ""}
-
- ${item.why_it_matters ? `
-
-
Why This Matters
-
${item.why_it_matters}
-
` : ""}
-
-
-
Source
-
- ${item.source_name}${item.published ? " · " + item.published : ""}
- ${sourceLink ? `
${sourceLink}` : ""}
-
-
- `;
-
- showDetailScreen();
-}
-
-async function showSeasonalResources() {
- resultsContainer.style.display = "none";
- newsResultsContainer.style.display = "none";
- essentialsResultsContainer.style.display = "none";
- detailScreen.style.display = "none";
-
- const carousel = document.getElementById("seasonal-carousel");
- carousel.innerHTML = `Loading...
`;
- seasonalResultsContainer.style.display = "block";
- showResultsScreen();
-
- try {
- const response = await fetch("/api/seasonal-images");
- const paths = await response.json();
- carousel.innerHTML = "";
- if (paths.length === 0) {
- carousel.innerHTML = `No community resources available.
`;
- } else {
- paths.forEach(src => {
- const filename = src.split("/").pop().replace(/\.[^.]+$/, "");
- carousel.innerHTML += `
-
-

-
${filename}
-
- `;
- });
- }
- } catch (error) {
- console.error("Failed to load seasonal images:", error);
- carousel.innerHTML = `Unable to load resources.
`;
- }
-}
-
-function showResourceDetails(resource) {
- const phone = resource.phones?.[0]?.number;
- const website = resource.websites?.[0]?.url;
- const location = resource.locations?.[0];
- const urgency = resource.urgency || "Standard";
-
- let addressHtml = "";
- if (location && !location.confidential) {
- if (location.address) {
- addressHtml = `${location.address}, ${location.city}, ${location.state} ${location.zip || ""}`;
- } else if (location.city) {
- addressHtml = `${location.city}, ${location.state}`;
- }
- }
-
- detailView.innerHTML = `
-
-
- ${resource.description || resource.summary ? `
-
-
${t("about")}
-
${resource.description || resource.summary}
-
` : ""}
-
- ${resource.eligibility ? `
-
-
${t("eligibility")}
-
${resource.eligibility}
-
` : ""}
-
- ${resource.category ? `
-
-
${t("category")}
-
${resource.category}${resource.subcategory ? " · " + resource.subcategory : ""}
-
` : ""}
-
- ${phone ? `
- ` : ""}
-
- ${addressHtml ? `
-
-
${t("address")}
-
${addressHtml}
-
` : ""}
-
- ${website ? `
- ` : ""}
- `;
-
- showDetailScreen();
-}
-
-// ===== Results Page AI Widget =====
-const resultsAiQuestion = document.getElementById("results-ai-question");
-const resultsAiSubmit = document.getElementById("results-ai-submit");
-const resultsAiOutput = document.getElementById("results-ai-output");
-
-async function submitResultsAi(query) {
- const userQuery = (query || resultsAiQuestion.value || "").trim();
- if (!userQuery) return;
- resultsAiQuestion.value = userQuery;
- resultsAiOutput.innerHTML = `
- `;
-
- try {
- const res = await fetch("/api/decide", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ userQuery, urgent: false, preferredCategories: [] })
- });
- if (!res.ok) throw new Error("HTTP " + res.status);
- const data = await res.json();
- renderResultsAiResponse(data);
- } catch (err) {
- resultsAiOutput.innerHTML = `Unable to get AI guidance: ${err.message}
`;
- }
-}
-
-function renderResultsAiResponse(data) {
- const title = data?.answerTitle || "Guidance";
- const steps = data?.steps || [];
- const notes = data?.notes || "";
-
- let stepsHtml = "";
- if (steps.length > 0) {
- stepsHtml = steps
- .sort((a, b) => (a.order || 0) - (b.order || 0))
- .map((s, i) => `
-
-
${i + 1}
-
-
${s.title || "Step"}: ${s.action || ""}
- ${s.why ? `
Why: ${s.why}
` : ""}
-
-
- `).join("");
- } else {
- stepsHtml = `${notes || "No specific steps available for this query."}
`;
- }
-
- resultsAiOutput.innerHTML = `
-
-
- ${notes && steps.length > 0 ? `
${notes}
` : ""}
-
${stepsHtml}
-
- `;
-}
-
-resultsAiSubmit.addEventListener("click", () => submitResultsAi());
-resultsAiQuestion.addEventListener("keydown", (e) => { if (e.key === "Enter") submitResultsAi(); });
-
-document.querySelectorAll(".results-ai-chip").forEach(chip => {
- chip.addEventListener("click", () => {
- const query = chip.getAttribute("data-query");
- submitResultsAi(query);
- chip.closest(".results-ai-chips").querySelectorAll(".results-ai-chip").forEach(c => c.classList.remove("selected"));
- chip.classList.add("selected");
- });
-});
-
-document.getElementById("results-ai-reset").addEventListener("click", () => {
- resultsAiQuestion.value = "";
- resultsAiOutput.innerHTML = "";
- document.querySelectorAll(".results-ai-chip").forEach(c => c.classList.remove("selected"));
- resultsAiQuestion.focus();
-});
diff --git a/backend/target/classes/static/images/seasonal/Disablility Info.jpg b/backend/target/classes/static/images/seasonal/Disablility Info.jpg
deleted file mode 100644
index 7c9456b..0000000
Binary files a/backend/target/classes/static/images/seasonal/Disablility Info.jpg and /dev/null differ
diff --git a/backend/target/classes/static/images/seasonal/Eviction Help.png b/backend/target/classes/static/images/seasonal/Eviction Help.png
deleted file mode 100644
index cd7568e..0000000
Binary files a/backend/target/classes/static/images/seasonal/Eviction Help.png and /dev/null differ
diff --git a/backend/target/classes/static/images/seasonal/Fundraiser.jpg b/backend/target/classes/static/images/seasonal/Fundraiser.jpg
deleted file mode 100644
index 1b2a9d7..0000000
Binary files a/backend/target/classes/static/images/seasonal/Fundraiser.jpg and /dev/null differ
diff --git a/backend/target/classes/static/images/seasonal/Furniture.jpg b/backend/target/classes/static/images/seasonal/Furniture.jpg
deleted file mode 100644
index 777e1ed..0000000
Binary files a/backend/target/classes/static/images/seasonal/Furniture.jpg and /dev/null differ
diff --git a/backend/target/classes/static/images/seasonal/Health Fair.jpg b/backend/target/classes/static/images/seasonal/Health Fair.jpg
deleted file mode 100644
index df28350..0000000
Binary files a/backend/target/classes/static/images/seasonal/Health Fair.jpg and /dev/null differ
diff --git a/backend/target/classes/static/images/seasonal/Volunteer.jpg b/backend/target/classes/static/images/seasonal/Volunteer.jpg
deleted file mode 100644
index e5efb28..0000000
Binary files a/backend/target/classes/static/images/seasonal/Volunteer.jpg and /dev/null differ
diff --git a/backend/target/classes/static/images/seasonal/Youth.jpg b/backend/target/classes/static/images/seasonal/Youth.jpg
deleted file mode 100644
index 26ba974..0000000
Binary files a/backend/target/classes/static/images/seasonal/Youth.jpg and /dev/null differ
diff --git a/backend/target/classes/static/index.html b/backend/target/classes/static/index.html
deleted file mode 100644
index 14b36e3..0000000
--- a/backend/target/classes/static/index.html
+++ /dev/null
@@ -1,193 +0,0 @@
-
-
-
-
-
- First Step - Trusted local guidance for Wilmington
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
What do you need help with today?
-
Find housing, essentials, community programs and local updates in one trusted place.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- AI Guidance
- Ask a question in natural language to find resources
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Choose filters to narrow results.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Community Resources
- Programs, Events and Community Opportunities
-
-
-
-
-
-
-
- Ask a question or pick a topic below:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/backend/target/classes/static/styles.css b/backend/target/classes/static/styles.css
deleted file mode 100644
index 48b0f4d..0000000
--- a/backend/target/classes/static/styles.css
+++ /dev/null
@@ -1,983 +0,0 @@
-/* ===== Root & Base ===== */
-:root {
- --primary-color: #1a5c38;
- --primary-light: #2d7d4a;
- --accent-color: #e07b39;
- --success-color: #2d7d4a;
- --warning-color: #d97706;
- --error-color: #b91c1c;
- --bg-light: #f5f0eb;
- --bg-lighter: #faf8f5;
- --border-color: #e5ddd5;
- --text-primary: #1c1917;
- --text-secondary: #6b7280;
- --text-light: #9ca3af;
- --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.05);
- --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
- --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1);
- --transition: all 0.2s ease;
- --radius: 12px;
-}
-
-* { box-sizing: border-box; }
-html { scroll-behavior: smooth; }
-
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
- background-color: var(--bg-lighter);
- color: var(--text-primary);
- margin: 0;
- padding: 0;
- line-height: 1.6;
- font-size: 1rem;
-}
-
-/* ===== Header ===== */
-.site-header {
- background: white;
- border-bottom: 1px solid var(--border-color);
- position: sticky;
- top: 0;
- z-index: 100;
- box-shadow: var(--shadow-sm);
-}
-
-.header-content {
- display: flex;
- justify-content: space-between;
- align-items: center;
- max-width: 1200px;
- margin: 0 auto;
- padding: 14px 24px;
- gap: 20px;
- flex-wrap: wrap;
-}
-
-.header-branding { flex: 0 0 auto; }
-
-.logo-link {
- text-decoration: none;
- color: inherit;
- display: flex;
- align-items: center;
- gap: 10px;
- cursor: pointer;
-}
-
-.logo {
- margin: 0;
- font-size: 22px;
- font-weight: 700;
- color: var(--primary-color);
- line-height: 1.2;
-}
-
-.logo-tagline {
- margin: 0;
- font-size: 12px;
- color: var(--text-secondary);
- font-weight: 400;
-}
-
-.ai-banner-header {
- display: flex;
- align-items: center;
- gap: 8px;
- background: linear-gradient(135deg, #fef3e8 0%, #fff8f0 100%);
- padding: 8px 16px;
- border-radius: 8px;
- font-size: 13px;
- color: var(--primary-color);
- font-weight: 500;
- white-space: nowrap;
- border: 1px solid #f5c89a;
- cursor: pointer;
- transition: var(--transition);
-}
-
-.ai-banner-header:hover {
- background: linear-gradient(135deg, #fde8cc 0%, #fef3e8 100%);
- border-color: var(--accent-color);
- box-shadow: var(--shadow-sm);
-}
-
-.ai-icon { font-size: 16px; }
-
-.header-utilities {
- flex: 0 0 auto;
- display: flex;
- gap: 6px;
- align-items: center;
-}
-
-.utility-button {
- padding: 7px 11px;
- border: 1px solid var(--border-color);
- background: white;
- border-radius: 6px;
- cursor: pointer;
- font-size: 13px;
- font-weight: 500;
- color: var(--text-primary);
- transition: var(--transition);
- min-width: 34px;
- text-align: center;
-}
-
-.utility-button:hover { background: var(--bg-light); border-color: var(--text-light); }
-.utility-button:active { background: var(--primary-color); color: white; border-color: var(--primary-color); }
-
-/* ===== Navigation ===== */
-.main-nav {
- background: var(--primary-color);
- border-bottom: 3px solid var(--accent-color);
- position: sticky;
- top: 61px;
- z-index: 99;
-}
-
-.nav-content {
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 24px;
- display: flex;
-}
-
-.nav-link {
- color: rgba(255,255,255,0.85);
- text-decoration: none;
- padding: 11px 20px;
- display: flex;
- align-items: center;
- font-weight: 500;
- font-size: 14px;
- border-bottom: 3px solid transparent;
- transition: var(--transition);
- cursor: pointer;
-}
-
-.nav-link:hover { background: rgba(255,255,255,0.1); color: white; }
-.nav-link.active { border-bottom-color: var(--accent-color); background: rgba(255,255,255,0.12); color: white; }
-
-/* ===== Container ===== */
-.container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 24px;
-}
-
-/* ===== Hero ===== */
-.hero-section {
- background: linear-gradient(135deg, #1a5c38 0%, #2d7d4a 60%, #e07b39 100%);
- color: white;
- padding: 40px 32px;
- border-radius: var(--radius);
- margin-bottom: 28px;
- text-align: center;
-}
-
-.hero-title {
- margin: 0 0 10px 0;
- font-size: 30px;
- font-weight: 700;
- line-height: 1.3;
-}
-
-.hero-subtitle {
- margin: 0;
- font-size: 17px;
- opacity: 0.92;
- line-height: 1.5;
-}
-
-/* ===== Home Grid ===== */
-.home-grid {
- display: grid;
- grid-template-columns: 2fr 1fr;
- gap: 24px;
- align-items: start;
-}
-
-.home-main { min-width: 0; }
-.home-side { min-width: 0; }
-
-/* ===== Category Cards ===== */
-.category-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
-}
-
-.category-card {
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 18px 20px;
- background: white;
- border: 1px solid var(--border-color);
- border-left: 4px solid var(--accent-color);
- border-radius: var(--radius);
- cursor: pointer;
- transition: var(--transition);
- font-size: 15px;
- text-align: left;
- width: 100%;
-}
-
-.category-card:hover {
- box-shadow: var(--shadow-md);
- transform: translateY(-1px);
- border-left-color: var(--primary-color);
-}
-
-.category-card:active { transform: translateY(0); }
-
-.category-icon { font-size: 26px; flex-shrink: 0; width: 36px; text-align: center; }
-
-.category-card h3 {
- margin: 0 0 4px 0;
- font-size: 15px;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.category-card p {
- margin: 0;
- font-size: 13px;
- color: var(--text-secondary);
- line-height: 1.4;
-}
-
-/* ===== AI Guidance Section ===== */
-.ai-guidance-section {
- background: linear-gradient(135deg, #fef3e8 0%, #fff8f0 100%);
- border: 1px solid #f5c89a;
- border-radius: var(--radius);
- padding: 22px;
- margin-top: 24px;
-}
-
-.ai-guidance-section h3 {
- margin: 0 0 5px 0;
- font-size: 17px;
- font-weight: 600;
- color: var(--primary-color);
-}
-
-.ai-guidance-subtitle {
- margin: 0 0 14px 0;
- font-size: 13px;
- color: var(--text-secondary);
-}
-
-.ai-input-group { display: flex; gap: 8px; margin-bottom: 10px; }
-
-.ai-input {
- flex: 1;
- padding: 10px 14px;
- border: 1px solid #f5c89a;
- border-radius: 8px;
- font-size: 14px;
- background: white;
- transition: var(--transition);
-}
-
-.ai-input:focus {
- outline: none;
- border-color: var(--accent-color);
- box-shadow: 0 0 0 3px rgba(224,123,57,0.15);
-}
-
-.ai-submit-btn {
- padding: 10px 20px;
- background: var(--accent-color);
- color: white;
- border: none;
- border-radius: 8px;
- font-weight: 600;
- cursor: pointer;
- transition: var(--transition);
- font-size: 14px;
- white-space: nowrap;
-}
-
-.ai-submit-btn:hover { background: #c4662a; }
-.ai-submit-btn:active { background: var(--primary-color); }
-
-.ai-reset-btn {
- padding: 10px 16px;
- background: white;
- color: var(--text-secondary);
- border: 1px solid var(--border-color);
- border-radius: 8px;
- font-weight: 500;
- cursor: pointer;
- transition: var(--transition);
- font-size: 14px;
- white-space: nowrap;
-}
-
-.ai-reset-btn:hover { background: var(--bg-light); border-color: var(--text-light); color: var(--text-primary); }
-
-.card-source-link {
- color: var(--accent-color);
- text-decoration: none;
- font-weight: 500;
-}
-
-.card-source-link:hover { text-decoration: underline; }
-
-.detail-source-link {
- display: inline-block;
- margin-top: 8px;
- color: white;
- background: var(--accent-color);
- padding: 7px 16px;
- border-radius: 6px;
- text-decoration: none;
- font-weight: 600;
- font-size: 14px;
- transition: var(--transition);
-}
-
-.detail-source-link:hover { background: #c4662a; }
-
-.ai-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
-
-.ai-chip {
- padding: 5px 12px;
- background: white;
- border: 1px solid #f5c89a;
- border-radius: 20px;
- cursor: pointer;
- font-size: 13px;
- transition: var(--transition);
-}
-
-.ai-chip:hover { background: #fef3e8; }
-.ai-chip.selected { background: var(--accent-color); color: white; border-color: var(--accent-color); }
-
-/* ===== AI Output ===== */
-.ai-output {
- margin-top: 12px;
-}
-
-.ai-loading {
- display: flex;
- align-items: center;
- gap: 12px;
- background: white;
- padding: 14px 16px;
- border-radius: 8px;
- font-size: 14px;
- color: var(--text-secondary);
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-.spinner {
- width: 18px;
- height: 18px;
- border: 2px solid #fef3e8;
- border-top-color: var(--accent-color);
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- flex-shrink: 0;
-}
-
-.ai-error {
- color: var(--error-color);
- font-size: 14px;
- background: #fef2f2;
- padding: 10px 14px;
- border-radius: 8px;
- border: 1px solid #fecaca;
- margin: 0;
-}
-
-/* ===== AI Response Card ===== */
-.ai-response-card {
- background: white;
- border-radius: 10px;
- padding: 16px;
- border: 1px solid #f5c89a;
-}
-
-.ai-response-header {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- margin-bottom: 12px;
- padding-bottom: 10px;
- border-bottom: 1px solid var(--border-color);
-}
-
-.ai-response-icon { font-size: 20px; flex-shrink: 0; margin-top: 1px; }
-
-.ai-response-title {
- margin: 0;
- font-size: 16px;
- font-weight: 600;
- color: var(--primary-color);
- line-height: 1.3;
-}
-
-.ai-response-notes {
- margin: 0 0 12px 0;
- font-size: 13px;
- color: var(--text-secondary);
- font-style: italic;
-}
-
-.ai-steps { display: flex; flex-direction: column; gap: 10px; }
-
-.ai-step {
- display: flex;
- gap: 12px;
- align-items: flex-start;
-}
-
-.ai-step-number {
- width: 24px;
- height: 24px;
- border-radius: 50%;
- background: var(--accent-color);
- color: white;
- font-size: 12px;
- font-weight: 700;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- margin-top: 2px;
-}
-
-.ai-step-body { flex: 1; }
-
-.ai-step-title {
- font-size: 14px;
- font-weight: 500;
- color: var(--text-primary);
- line-height: 1.4;
-}
-
-.ai-step-why {
- font-size: 13px;
- color: var(--text-secondary);
- margin-top: 3px;
- line-height: 1.4;
-}
-
-.ai-no-steps {
- font-size: 14px;
- color: var(--text-secondary);
- margin: 0;
-}
-
-.ai-citations {
- margin-top: 12px;
- padding-top: 10px;
- border-top: 1px solid var(--border-color);
- font-size: 13px;
- color: var(--text-secondary);
-}
-
-.ai-citations ul {
- margin: 6px 0 0 0;
- padding-left: 18px;
-}
-
-.ai-citations li { margin: 3px 0; }
-
-/* ===== Latest Updates Sidebar ===== */
-.resource-panel {
- background: white;
- border: 1px solid var(--border-color);
- border-radius: var(--radius);
- padding: 18px;
- box-shadow: var(--shadow-sm);
- position: sticky;
- top: 160px;
-}
-
-.resource-panel h3 {
- margin: 0 0 14px 0;
- font-size: 15px;
- font-weight: 600;
- color: var(--text-primary);
- padding-bottom: 10px;
- border-bottom: 2px solid var(--accent-color);
-}
-
-.news-list { display: flex; flex-direction: column; gap: 10px; }
-
-.news-item {
- padding: 10px 12px;
- background: var(--bg-lighter);
- border-radius: 8px;
- border-left: 3px solid var(--accent-color);
- cursor: pointer;
- transition: var(--transition);
-}
-
-.news-item:hover { background: var(--bg-light); transform: translateX(3px); }
-
-.news-item h4 {
- margin: 0 0 3px 0;
- font-size: 13px;
- font-weight: 600;
- color: var(--text-primary);
- line-height: 1.3;
-}
-
-.news-item .news-date { font-size: 11px; color: var(--text-light); }
-
-/* ===== Filter Screen ===== */
-#filter-screen {
- background: white;
- border-radius: var(--radius);
- padding: 24px;
- border: 1px solid var(--border-color);
- margin-top: 8px;
-}
-
-.screen-header {
- display: flex;
- align-items: center;
- gap: 14px;
- margin-bottom: 16px;
-}
-
-.screen-header h2 { margin: 0; font-size: 22px; font-weight: 700; }
-
-.back-button {
- padding: 7px 14px;
- background: white;
- border: 1px solid var(--border-color);
- border-radius: 8px;
- cursor: pointer;
- font-size: 14px;
- transition: var(--transition);
- color: var(--text-primary);
- white-space: nowrap;
-}
-
-.back-button:hover { background: var(--bg-light); border-color: var(--primary-color); color: var(--primary-color); }
-
-.screen-subtitle { color: var(--text-secondary); font-size: 14px; margin: 0 0 18px 0; }
-
-.filter-options { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 22px; }
-
-.filter-chip {
- padding: 9px 16px;
- background: white;
- border: 2px solid var(--border-color);
- border-radius: 24px;
- cursor: pointer;
- font-size: 14px;
- font-weight: 500;
- transition: var(--transition);
- color: var(--text-primary);
-}
-
-.filter-chip:hover { border-color: var(--accent-color); background: #fef3e8; color: var(--primary-color); }
-.filter-chip.selected { background: var(--accent-color); color: white; border-color: var(--accent-color); }
-
-.primary-button {
- padding: 11px 24px;
- background: var(--primary-color);
- color: white;
- border: none;
- border-radius: 8px;
- font-weight: 600;
- cursor: pointer;
- font-size: 15px;
- transition: var(--transition);
-}
-
-.primary-button:hover { background: var(--primary-light); }
-
-/* ===== Results Screen ===== */
-.results-header { display: flex; align-items: center; gap: 14px; margin-bottom: 20px; }
-
-.results-list { display: flex; flex-direction: column; gap: 0; }
-
-/* ===== Resource Cards ===== */
-.resource-card {
- background: white;
- border: 1px solid var(--border-color);
- border-left: 4px solid var(--accent-color);
- border-radius: var(--radius);
- padding: 16px 18px;
- cursor: pointer;
- transition: var(--transition);
- margin-bottom: 12px;
-}
-
-.resource-card:hover {
- box-shadow: var(--shadow-md);
- transform: translateY(-2px);
- border-left-color: var(--primary-color);
-}
-
-.resource-card:active { transform: translateY(0); }
-
-.card-top {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 12px;
- margin-bottom: 6px;
-}
-
-.card-title {
- margin: 0;
- font-size: 15px;
- font-weight: 600;
- color: var(--primary-color);
- line-height: 1.3;
- flex: 1;
-}
-
-.card-summary {
- margin: 0 0 8px 0;
- font-size: 13px;
- color: var(--text-secondary);
- line-height: 1.5;
-}
-
-.card-phone {
- margin: 0 0 6px 0;
- font-size: 13px;
-}
-
-.card-phone a {
- color: var(--accent-color);
- text-decoration: none;
- font-weight: 500;
-}
-
-.card-phone a:hover { text-decoration: underline; }
-
-.card-why {
- margin: 4px 0;
- font-size: 13px;
- color: var(--text-secondary);
- line-height: 1.4;
-}
-
-.card-source {
- margin: 6px 0 0 0;
- font-size: 11px;
- color: var(--text-light);
-}
-
-.card-cta {
- margin: 8px 0 0 0;
- font-size: 12px;
- font-weight: 600;
- color: var(--accent-color);
- letter-spacing: 0.2px;
-}
-
-.empty-state {
- color: var(--text-secondary);
- font-size: 14px;
- padding: 20px;
- text-align: center;
-}
-
-/* ===== Urgency Tags ===== */
-.urgency-tag {
- display: inline-block;
- padding: 3px 10px;
- border-radius: 12px;
- font-size: 11px;
- font-weight: 600;
- white-space: nowrap;
- flex-shrink: 0;
-}
-
-.urgency-emergency { background: #fef2f2; color: #991b1b; }
-.urgency-time-limited { background: #fffbeb; color: #92400e; }
-.urgency-standard { background: #f0fdf4; color: #166534; }
-
-/* ===== Page Headers ===== */
-.page-header {
- margin-bottom: 20px;
- padding-bottom: 16px;
- border-bottom: 2px solid var(--border-color);
-}
-
-.page-header h2 {
- margin: 0 0 6px 0;
- font-size: 22px;
- font-weight: 700;
- color: var(--primary-color);
-}
-
-.page-description {
- color: var(--text-secondary);
- font-size: 14px;
- line-height: 1.5;
- margin: 0;
-}
-
-/* ===== Category Group Headers ===== */
-.category-group-header {
- margin: 22px 0 8px;
- font-size: 14px;
- font-weight: 600;
- color: var(--primary-color);
- text-transform: uppercase;
- letter-spacing: 0.5px;
- border-bottom: 1px solid var(--border-color);
- padding-bottom: 6px;
-}
-
-/* ===== Detail Screen ===== */
-#detail-screen { padding-top: 4px; }
-
-.detail-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 12px;
- margin-bottom: 20px;
- padding-bottom: 16px;
- border-bottom: 2px solid var(--border-color);
-}
-
-.detail-org {
- margin: 0;
- font-size: 22px;
- font-weight: 700;
- color: var(--primary-color);
- flex: 1;
- line-height: 1.3;
-}
-
-.detail-section {
- padding: 14px 0;
- border-bottom: 1px solid var(--border-color);
-}
-
-.detail-section:last-child { border-bottom: none; }
-
-.detail-label {
- font-size: 11px;
- font-weight: 700;
- color: var(--text-light);
- text-transform: uppercase;
- letter-spacing: 0.6px;
- margin-bottom: 5px;
-}
-
-.detail-value {
- font-size: 15px;
- color: var(--text-primary);
- line-height: 1.5;
-}
-
-.detail-value a, .detail-phone-link {
- color: var(--accent-color);
- text-decoration: none;
- font-weight: 500;
-}
-
-.detail-value a:hover, .detail-phone-link:hover { text-decoration: underline; }
-
-/* ===== Seasonal Carousel ===== */
-#seasonal-carousel {
- display: flex;
- gap: 14px;
- overflow-x: auto;
- padding: 8px 0 16px 0;
- scroll-snap-type: x mandatory;
- -webkit-overflow-scrolling: touch;
-}
-
-#seasonal-carousel::-webkit-scrollbar { height: 5px; }
-#seasonal-carousel::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 4px; }
-
-.carousel-card {
- flex-shrink: 0;
- width: 75%;
- scroll-snap-align: start;
- border-radius: var(--radius);
- overflow: hidden;
- border: 1px solid var(--border-color);
- background: white;
- box-shadow: var(--shadow-sm);
-}
-
-.carousel-card img {
- width: 100%;
- height: 420px;
- object-fit: contain;
- display: block;
- background: #f9fafb;
-}
-
-.carousel-caption {
- padding: 10px 14px;
- font-size: 13px;
- color: var(--text-secondary);
-}
-
-/* ===== Essentials Category Nav ===== */
-.essentials-category-nav {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- align-items: center;
- margin-bottom: 20px;
- padding: 12px 16px;
- background: var(--bg-light);
- border-radius: 8px;
- border: 1px solid var(--border-color);
-}
-
-.essentials-nav-label {
- font-size: 12px;
- font-weight: 600;
- color: var(--text-secondary);
- text-transform: uppercase;
- letter-spacing: 0.5px;
- margin-right: 4px;
-}
-
-.essentials-nav-link {
- display: inline-block;
- padding: 5px 12px;
- background: white;
- border: 1px solid var(--border-color);
- border-radius: 20px;
- font-size: 13px;
- font-weight: 500;
- color: var(--primary-color);
- text-decoration: none;
- transition: var(--transition);
-}
-
-.essentials-nav-link:hover {
- background: var(--accent-color);
- color: white;
- border-color: var(--accent-color);
-}
-
-/* ===== Results Page AI Widget ===== */
-.results-ai-section {
- background: linear-gradient(135deg, #fef3e8 0%, #fff8f0 100%);
- border: 1px solid #f5c89a;
- border-radius: var(--radius);
- padding: 20px 22px;
- margin-top: 28px;
-}
-
-.results-ai-header {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 4px;
-}
-
-.results-ai-header h4 {
- margin: 0;
- font-size: 15px;
- font-weight: 600;
- color: var(--primary-color);
-}
-
-.results-ai-subtitle {
- margin: 0 0 12px 0;
- font-size: 13px;
- color: var(--text-secondary);
-}
-
-.results-ai-chips {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-bottom: 12px;
-}
-
-.results-ai-chip {
- padding: 6px 14px;
- background: white;
- border: 1px solid #f5c89a;
- border-radius: 20px;
- cursor: pointer;
- font-size: 13px;
- transition: var(--transition);
- color: var(--text-primary);
-}
-
-.results-ai-chip:hover { background: #fef3e8; border-color: var(--accent-color); }
-.results-ai-chip.selected { background: var(--accent-color); color: white; border-color: var(--accent-color); }
-
-.results-ai-input-row {
- display: flex;
- gap: 8px;
- margin-bottom: 10px;
-}
-
-.results-ai-input-row .ai-input { flex: 1; }
-
-/* ===== High Contrast ===== */
-body.high-contrast { background-color: #000; color: #fff; }
-
-body.high-contrast .site-header,
-body.high-contrast .resource-panel,
-body.high-contrast .category-card,
-body.high-contrast .ai-guidance-section,
-body.high-contrast #filter-screen,
-body.high-contrast .resource-card,
-body.high-contrast .ai-response-card {
- background-color: #000;
- color: #fff;
- border-color: #fff;
-}
-
-body.high-contrast .main-nav { background-color: #111; border-color: #ff0; }
-body.high-contrast .nav-link { color: #fff; }
-body.high-contrast .nav-link.active { border-bottom-color: #ff0; }
-body.high-contrast .category-card { border-left-color: #ff0; }
-body.high-contrast .resource-card { border-left-color: #ff0; }
-body.high-contrast .urgency-tag { background: #333; color: #ff0; }
-body.high-contrast a, body.high-contrast .card-phone a, body.high-contrast .detail-value a { color: #ff0; }
-body.high-contrast .utility-button,
-body.high-contrast .filter-chip,
-body.high-contrast .ai-chip,
-body.high-contrast .back-button { background: #000; border-color: #fff; color: #fff; }
-body.high-contrast .ai-input { background: #000; border-color: #fff; color: #fff; }
-body.high-contrast .page-header, body.high-contrast .detail-header, body.high-contrast .detail-section { border-color: #555; }
-body.high-contrast .detail-label { color: #aaa; }
-body.high-contrast .card-cta { color: #ff0; }
-
-/* ===== Responsive ===== */
-@media (max-width: 768px) {
- .header-content { flex-direction: column; gap: 10px; padding: 12px 16px; }
- .home-grid { grid-template-columns: 1fr; }
- .home-side { display: none; }
- .hero-title { font-size: 24px; }
- .hero-subtitle { font-size: 15px; }
- .hero-section { padding: 28px 20px; }
- .ai-banner-header { display: none; }
- .ai-input-group { flex-direction: column; }
- .container { padding: 16px; }
- .carousel-card { width: 88%; }
- .nav-content { overflow-x: auto; }
- .nav-link { padding: 10px 14px; font-size: 13px; }
- .card-top { flex-direction: column; gap: 6px; }
- .detail-org { font-size: 18px; }
- .main-nav { top: 52px; }
-}
-
-/* ===== Print ===== */
-@media print {
- .site-header, .main-nav, .back-button { display: none; }
- .container { max-width: 100%; padding: 0; }
-}
diff --git a/backend/target/firststep-backend-0.1.0.jar b/backend/target/firststep-backend-0.1.0.jar
deleted file mode 100644
index 123e7af..0000000
Binary files a/backend/target/firststep-backend-0.1.0.jar and /dev/null differ
diff --git a/backend/target/firststep-backend-0.1.0.jar.original b/backend/target/firststep-backend-0.1.0.jar.original
deleted file mode 100644
index 5e4407d..0000000
Binary files a/backend/target/firststep-backend-0.1.0.jar.original and /dev/null differ
diff --git a/backend/target/maven-archiver/pom.properties b/backend/target/maven-archiver/pom.properties
deleted file mode 100644
index 91a095e..0000000
--- a/backend/target/maven-archiver/pom.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-artifactId=firststep-backend
-groupId=org.firststep
-version=0.1.0
diff --git a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
deleted file mode 100644
index 3b9d1a0..0000000
--- a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
+++ /dev/null
@@ -1,25 +0,0 @@
-org/firststep/backend/controller/DecisionController.class
-org/firststep/backend/service/NewsService.class
-org/firststep/backend/model/Resource$Website.class
-org/firststep/backend/dto/DecisionResponse.class
-org/firststep/backend/service/ResourceService$3.class
-org/firststep/backend/service/DecisionAgentService.class
-org/firststep/backend/service/OllamaService.class
-org/firststep/backend/service/NewsService$1.class
-org/firststep/backend/service/DecisionAgentService$ResourceScore.class
-org/firststep/backend/FirstStepApplication.class
-org/firststep/backend/controller/NewsController.class
-org/firststep/backend/controller/ResourceController.class
-org/firststep/backend/service/DecisionAgentService$NewsServiceLike.class
-org/firststep/backend/model/NewsItem.class
-org/firststep/backend/model/Resource$Phone.class
-org/firststep/backend/service/ResourceService$2.class
-org/firststep/backend/service/ResourceService$1.class
-org/firststep/backend/dto/DecisionRequest.class
-org/firststep/backend/service/DecisionAgentService$NewsScore.class
-org/firststep/backend/service/ResourceService.class
-org/firststep/backend/dto/DecisionStep.class
-org/firststep/backend/service/DecisionAgentService$ResourceServiceLike.class
-org/firststep/backend/dto/Citation.class
-org/firststep/backend/model/Resource$Location.class
-org/firststep/backend/model/Resource.class
diff --git a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
deleted file mode 100644
index 7ba7fc8..0000000
--- a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
+++ /dev/null
@@ -1,16 +0,0 @@
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/controller/DecisionController.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/service/ResourceService.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/service/OllamaService.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/dto/DecisionResponse.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/controller/NewsController.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/dto/DecisionRequest.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/model/Resource.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/dto/DecisionStep.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/service/NewsService.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/dto/Citation.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/service/RssFeedSource.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/FirstStepApplication.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/controller/ResourceController.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/service/DecisionAgentService.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/model/NewsItem.java
-/Users/anitra/Projects/FirstStep/backend/src/main/java/org/firststep/backend/service/RssFeedService.java
diff --git a/backend/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/backend/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/backend/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst
deleted file mode 100644
index a1268b5..0000000
--- a/backend/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst
+++ /dev/null
@@ -1 +0,0 @@
-/Users/anitra/Projects/FirstStep/backend/src/test/java/org/firststep/backend/controller/NewsControllerTest.java
diff --git a/backend/target/surefire-reports/TEST-org.firststep.backend.controller.NewsControllerTest.xml b/backend/target/surefire-reports/TEST-org.firststep.backend.controller.NewsControllerTest.xml
deleted file mode 100644
index cc3260d..0000000
--- a/backend/target/surefire-reports/TEST-org.firststep.backend.controller.NewsControllerTest.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/backend/target/surefire-reports/org.firststep.backend.controller.NewsControllerTest.txt b/backend/target/surefire-reports/org.firststep.backend.controller.NewsControllerTest.txt
deleted file mode 100644
index 643c5f9..0000000
--- a/backend/target/surefire-reports/org.firststep.backend.controller.NewsControllerTest.txt
+++ /dev/null
@@ -1,4 +0,0 @@
--------------------------------------------------------------------------------
-Test set: org.firststep.backend.controller.NewsControllerTest
--------------------------------------------------------------------------------
-Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.369 s - in org.firststep.backend.controller.NewsControllerTest
diff --git a/backend/target/test-classes/org/firststep/backend/controller/NewsControllerTest$TestConfig.class b/backend/target/test-classes/org/firststep/backend/controller/NewsControllerTest$TestConfig.class
deleted file mode 100644
index 0a64ebe..0000000
Binary files a/backend/target/test-classes/org/firststep/backend/controller/NewsControllerTest$TestConfig.class and /dev/null differ
diff --git a/backend/target/test-classes/org/firststep/backend/controller/NewsControllerTest.class b/backend/target/test-classes/org/firststep/backend/controller/NewsControllerTest.class
deleted file mode 100644
index 4bcf55f..0000000
Binary files a/backend/target/test-classes/org/firststep/backend/controller/NewsControllerTest.class and /dev/null differ
diff --git a/references/RssFeedService_annotated.java b/references/RssFeedService_annotated.java
new file mode 100644
index 0000000..7489e90
--- /dev/null
+++ b/references/RssFeedService_annotated.java
@@ -0,0 +1,379 @@
+package org.firststep.backend.service;
+
+// =============================================================================
+// WHAT THIS CLASS DOES
+// =============================================================================
+// RssFeedService fetches one or more RSS feeds on a schedule, converts each
+// entry into a NewsItem, classifies it by civic topic (housing, healthcare,
+// food, etc.), and holds the result in memory for the NewsController to serve
+// at GET /api/news/rss.
+//
+// It implements RssFeedSource so it can be swapped with a fake in tests
+// without needing Mockito on concrete classes.
+// =============================================================================
+
+import com.rometools.rome.feed.synd.*;
+import com.rometools.rome.io.SyndFeedInput;
+import com.rometools.rome.io.XmlReader;
+import org.firststep.backend.model.NewsItem;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.net.URL;
+import java.net.URLConnection;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class RssFeedService implements RssFeedSource {
+
+ private static final Logger log = LoggerFactory.getLogger(RssFeedService.class);
+
+ // WHY: Comma-separated URL list from application.properties so URLs can be
+ // changed or added without recompiling.
+ // Configured as: news.rss.urls=https://legis.delaware.gov/rss/RssFeeds/GovernorSignedLegislation
+ @Value("${news.rss.urls:}")
+ private String rssFeedUrls;
+
+ // WHY: volatile ensures the list is visible across threads when @Scheduled
+ // writes it and the request thread reads it concurrently.
+ private volatile List rssItems = List.of();
+
+ private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("yyyy-MM-dd");
+
+ // =============================================================================
+ // SCHEDULED FETCH
+ // =============================================================================
+ // WHY initialDelayString vs initialDelay: The property-driven form allows the
+ // delay to be tuned in tests and deployment without recompiling. Default 500ms
+ // is fast enough that items appear before the user first loads the page.
+ // WHY fixedDelay (not fixedRate): fixedDelay waits after the previous run
+ // completes, avoiding overlap if a fetch takes longer than the interval.
+ @Scheduled(
+ fixedDelayString = "${news.rss.refresh-interval:3600000}",
+ initialDelayString = "${news.rss.initial-delay:500}"
+ )
+ public void fetchFeeds() {
+ if (rssFeedUrls == null || rssFeedUrls.isBlank()) {
+ log.warn("RSS: No feed URLs configured");
+ return;
+ }
+
+ List collected = new ArrayList<>();
+
+ for (String rawUrl : rssFeedUrls.split(",")) {
+ String url = rawUrl.trim();
+ if (url.isEmpty()) continue;
+
+ try {
+ SyndFeed feed = loadFeed(url);
+ if (feed == null) continue;
+
+ for (SyndEntry entry : feed.getEntries()) {
+ NewsItem item = convertEntry(feed, entry);
+ collected.add(item);
+ }
+
+ log.info("RSS: Loaded {} entries from {}", feed.getEntries().size(), url);
+
+ } catch (Exception ex) {
+ log.error("RSS: Failed to load {} → {}", url, ex.getMessage());
+ }
+ }
+
+ // WHY only replace when non-empty: a transient network failure during a
+ // refresh should not wipe out the last good result.
+ if (!collected.isEmpty()) {
+ rssItems = List.copyOf(collected);
+ }
+ }
+
+ public List getRssItems() {
+ return rssItems;
+ }
+
+ // ------------------------------------------------------------
+ // Helpers
+ // ------------------------------------------------------------
+
+ // WHY setAllowDoctypes(true): Delaware's feed includes a DOCTYPE declaration
+ // which JAXP blocks by default as a security measure. We allow it here
+ // because this is a trusted government feed.
+ private SyndFeed loadFeed(String url) {
+ try {
+ URL u = new URL(url);
+ URLConnection conn = u.openConnection();
+ conn.setRequestProperty("User-Agent", "Mozilla/5.0 (RSS Reader)");
+ conn.setConnectTimeout(8000);
+ conn.setReadTimeout(8000);
+
+ SyndFeedInput input = new SyndFeedInput();
+ input.setAllowDoctypes(true);
+ return input.build(new XmlReader(conn));
+
+ } catch (Exception e) {
+ log.error("RSS: Error reading {} → {}", url, e.getMessage());
+ return null;
+ }
+ }
+
+ // =============================================================================
+ // ENTRY → NewsItem CONVERSION
+ // =============================================================================
+ // HOW IT WORKS:
+ // 1. Map raw RSS fields (title, description, link, date) to NewsItem fields.
+ // 2. Run keyword classification to assign civic category tags and a generic
+ // "why it matters" sentence.
+ // 3. Try to extract a "RELATING TO …" clause from the description. Delaware
+ // legislation descriptions always begin with the full act title in ALL CAPS,
+ // e.g. "AN ACT TO AMEND TITLE 1 … RELATING TO PUERTO RICO DAY." If found,
+ // this clause (converted to Sentence Case) replaces both the bill-number
+ // headline and the generic why-it-matters text, giving users a readable,
+ // specific description of the law.
+ private NewsItem convertEntry(SyndFeed feed, SyndEntry entry) {
+ NewsItem item = new NewsItem();
+
+ item.id = "rss-" + UUID.randomUUID();
+ item.headline = safe(entry.getTitle()); // bill number e.g. "HB 311" — overridden below if RELATING TO found
+ item.summary = extractSummary(entry);
+ item.body = item.summary;
+
+ item.sourceName = feed.getTitle() != null ? feed.getTitle() : "RSS Feed";
+ item.sourceUrl = entry.getLink();
+
+ Date published = entry.getPublishedDate() != null
+ ? entry.getPublishedDate()
+ : entry.getUpdatedDate() != null
+ ? entry.getUpdatedDate()
+ : new Date();
+
+ item.published = DATE_FMT.format(published);
+
+ item.active = true;
+ item.type = "legislation";
+ item.urgency = "standard";
+
+ // Step 2: keyword classification
+ String text = (item.headline + " " + item.summary).toLowerCase();
+ Classification cls = classifyLegislation(text);
+ item.categoryTags = cls.categoryTags;
+ item.resourceTags = cls.resourceTags;
+ item.whyItMatters = cls.whyItMatters;
+
+ // Step 3: extract "RELATING TO …" clause for a more readable headline/why
+ String relatingTo = extractRelatingTo(item.summary);
+ if (relatingTo != null) {
+ item.headline = relatingTo;
+ item.whyItMatters = relatingTo;
+ }
+
+ return item;
+ }
+
+ // =============================================================================
+ // RELATING TO EXTRACTION
+ // =============================================================================
+ // WHY: Delaware legislation descriptions always contain a formal title in ALL
+ // CAPS like "AN ACT TO AMEND … RELATING TO PAID LEAVE." The "RELATING TO"
+ // clause is the most human-readable part. We extract it and convert to
+ // Sentence Case so the UI can show "Relating to paid leave." instead of a
+ // bill number or a generic category sentence.
+ //
+ // ALGORITHM — three terminators, earliest wins:
+ // 1. First "." after "RELATING TO" — normal case where act title ends with a period.
+ // 2. "This " boundary — Delaware descriptions follow the act title with
+ // "This Act…", "This Senate…", etc. When there is no period before this
+ // phrase, stop just before it and append a period.
+ // 3. 120-character cap — hard safety net, truncates at last word boundary.
+ // Returns null if "RELATING TO" is not found or nothing remains after trimming.
+ private static final int RELATING_TO_MAX_CHARS = 120;
+
+ private static String extractRelatingTo(String summary) {
+ if (summary == null) return null;
+ String upper = summary.toUpperCase();
+ int start = upper.indexOf("RELATING TO");
+ if (start < 0) return null;
+
+ int end = Integer.MAX_VALUE;
+
+ // Rule 1: first period after "RELATING TO" (include the period)
+ int periodIdx = summary.indexOf('.', start);
+ if (periodIdx >= 0) end = Math.min(end, periodIdx + 1);
+
+ // Rule 2: "This " boundary — stop before it
+ java.util.regex.Matcher m = java.util.regex.Pattern
+ .compile("\\bThis\\s+[A-Z]", java.util.regex.Pattern.CASE_INSENSITIVE)
+ .matcher(summary);
+ while (m.find()) {
+ if (m.start() > start) {
+ int boundary = m.start();
+ while (boundary > start && Character.isWhitespace(summary.charAt(boundary - 1))) {
+ boundary--;
+ }
+ end = Math.min(end, boundary);
+ break;
+ }
+ }
+
+ if (end == Integer.MAX_VALUE || end <= start) return null;
+
+ String raw = summary.substring(start, end).trim();
+
+ // Rule 3: character cap — truncate at last word boundary
+ if (raw.length() > RELATING_TO_MAX_CHARS) {
+ raw = raw.substring(0, RELATING_TO_MAX_CHARS);
+ int lastSpace = raw.lastIndexOf(' ');
+ if (lastSpace > 0) raw = raw.substring(0, lastSpace);
+ }
+
+ if (raw.isEmpty()) return null;
+
+ if (!raw.endsWith(".")) raw = raw + ".";
+
+ return Character.toUpperCase(raw.charAt(0)) + raw.substring(1).toLowerCase();
+ }
+
+ // =============================================================================
+ // CLASSIFICATION
+ // =============================================================================
+ // WHY LinkedHashMap: insertion order matters — the first matched tag determines
+ // which "why it matters" sentence is used when multiple tags match.
+ //
+ // HOW IT WORKS: Each entry's lowercase headline+summary is tested against
+ // keyword arrays. Any matching bucket adds its display tag to the result list.
+ // If no bucket matches, a generic "Delaware Legislation" tag and fallback
+ // sentence are returned.
+ private static final class Classification {
+ List categoryTags;
+ List resourceTags;
+ String whyItMatters;
+ Classification(List categoryTags, List resourceTags, String whyItMatters) {
+ this.categoryTags = categoryTags;
+ this.resourceTags = resourceTags;
+ this.whyItMatters = whyItMatters;
+ }
+ }
+
+ private static final Map TAG_KEYWORDS = new LinkedHashMap<>();
+ static {
+ TAG_KEYWORDS.put("housing", new String[]{"housing", "rent", "landlord", "tenant", "evict",
+ "mortgage", "residential", "manufactured home",
+ "affordable rental", "shelter"});
+ TAG_KEYWORDS.put("healthcare", new String[]{"health", "medical", "medicaid", "medicare",
+ "hospital", "clinic", "mental health", "prescription",
+ "nursing", "patient", "wellness", "behavioral health",
+ "opioid", "drug", "therapy", "physician", "care",
+ "insurance", "vaccination", "public health",
+ "drinking water", "long-term care", "school-based health"});
+ TAG_KEYWORDS.put("food", new String[]{"food", "nutrition", "snap", "hunger", "grocery",
+ "meal", "wic", "restaurant meals", "dietitian",
+ "farm", "agriculture"});
+ TAG_KEYWORDS.put("employment", new String[]{"employ", "worker", "wage", "labor", "job",
+ "workplace", "paid leave", "unemployment",
+ "workforce", "occupational", "salary", "licensure"});
+ TAG_KEYWORDS.put("utilities", new String[]{"utility", "utilities", "electric", "energy",
+ "net meter", "solar", "water system"});
+ TAG_KEYWORDS.put("disability", new String[]{"disability", "disabilities", "accessible", "accessibility",
+ "accommodation", "developmental disability",
+ "rehabilitation", "hearing", "blue envelope"});
+ TAG_KEYWORDS.put("benefits", new String[]{"benefit", "assistance", "subsidy", "aid",
+ "social service", "low-income", "poverty",
+ "state employee benefit", "child care",
+ "school-based", "voucher"});
+ TAG_KEYWORDS.put("legal", new String[]{"court", "justice", "civil right", "equal accommodation",
+ "protection", "eviction", "trafficking",
+ "stalking", "criminal", "juvenile"});
+ }
+
+ private static final Map TAG_WHY = new LinkedHashMap<>();
+ static {
+ TAG_WHY.put("housing", "This new law may affect your rights as a renter, homeowner, or manufactured-home resident in Delaware.");
+ TAG_WHY.put("healthcare", "This new law may change what health services or coverage are available to you or your family.");
+ TAG_WHY.put("food", "This new law may affect food assistance programs or nutrition services in your community.");
+ TAG_WHY.put("employment", "This new law may change your rights or benefits at work, including wages, leave, or licensing.");
+ TAG_WHY.put("utilities", "This new law may affect your electric, water, or energy bills.");
+ TAG_WHY.put("disability", "This new law may expand services or protections for people with disabilities.");
+ TAG_WHY.put("benefits", "This new law may change assistance programs or benefits available to low-income Delawareans.");
+ TAG_WHY.put("legal", "This new law may affect your legal rights or access to the courts.");
+ }
+
+ private static Classification classifyLegislation(String text) {
+ List matched = new ArrayList<>();
+ for (Map.Entry entry : TAG_KEYWORDS.entrySet()) {
+ for (String kw : entry.getValue()) {
+ if (text.contains(kw)) {
+ matched.add(entry.getKey());
+ break;
+ }
+ }
+ }
+
+ if (matched.isEmpty()) {
+ return new Classification(
+ List.of("Delaware Legislation"),
+ List.of(),
+ "Stay informed about new laws signed by the Governor of Delaware."
+ );
+ }
+
+ List categoryTags = matched.stream()
+ .map(t -> Character.toUpperCase(t.charAt(0)) + t.substring(1))
+ .collect(Collectors.toList());
+
+ List resourceTags = new ArrayList<>(matched);
+
+ String why = TAG_WHY.get(matched.get(0));
+
+ return new Classification(categoryTags, resourceTags, why);
+ }
+
+ // =============================================================================
+ // SUMMARY EXTRACTION
+ // =============================================================================
+ // WHY three sources: RSS feeds vary — most use , some use
+ // , and older feeds may use media extensions. Trying in order
+ // handles all three without configuration.
+ private String extractSummary(SyndEntry entry) {
+ // 1. Standard
+ if (entry.getDescription() != null && entry.getDescription().getValue() != null) {
+ return stripHtml(entry.getDescription().getValue());
+ }
+
+ // 2.
+ if (entry.getContents() != null && !entry.getContents().isEmpty()) {
+ SyndContent content = entry.getContents().get(0);
+ if (content != null && content.getValue() != null) {
+ return stripHtml(content.getValue());
+ }
+ }
+
+ // 3. Media module fallback
+ for (SyndContent c : entry.getContents()) {
+ if (c != null && c.getValue() != null) {
+ return stripHtml(c.getValue());
+ }
+ }
+
+ return "";
+ }
+
+ private static String safe(String s) {
+ return s == null ? "" : s.trim();
+ }
+
+ private static String stripHtml(String html) {
+ if (html == null) return "";
+ return html
+ .replaceAll("<[^>]+>", " ")
+ .replaceAll(" ", " ")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll("\\s+", " ")
+ .trim();
+ }
+}
diff --git a/references/architecture.md b/references/architecture.md
deleted file mode 100644
index e69de29..0000000
diff --git a/references/decisions.md b/references/decisions.md
index be40734..ca6037f 100644
--- a/references/decisions.md
+++ b/references/decisions.md
@@ -11,4 +11,23 @@ Examples:
- Domestic violence shelter
Conclusion:
-A filter stage is necessary before showing results.
\ No newline at end of file
+A filter stage is necessary before showing results.
+
+# Decision 004
+
+Two filesystem reads used relative paths hardcoded to the repo root:
+- ResourceService loaded `app/data/resources.json`
+- ResourceController listed `backend/src/main/resources/static/images/seasonal`
+
+This tied the app to being launched from the repo root and blocked deployment
+from any other working directory (e.g. a container).
+
+Conclusion:
+Made both paths configurable via `@Value` properties, keeping the original
+relative paths as defaults so existing behavior is unchanged:
+- `app.data.dir` (default `app/data`)
+- `app.seasonal.images.dir` (default `backend/src/main/resources/static/images/seasonal`)
+
+A different working directory now overrides these via env/properties without
+code changes. Chose `@Value` over `@ConfigurationProperties` to match the
+existing style (OllamaService already injects `ollama.api.url` via `@Value`).
\ No newline at end of file