Upgraded firmware for Simone Giertz's Every Day Calendar that links an ATProto-powered ESP32, for sync with goals.garden 🌱
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Show handle on web UI and highlight saved goal only

- Store handle from ATProto session response and add getHandle()
- Display handle instead of DID in web UI (link still goes to DID profile)
- Change green highlight to use .saved class instead of :has(input:checked)
so the highlight stays on the saved goal, not the currently checked radio

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+18 -8
+2 -2
firmware/esp32/GoalsGardenSync/GoalsGardenSync.ino
··· 101 101 102 102 if (atprotoConnected) { 103 103 // Start web server 104 - webServer.begin(&atproto, atproto.getDid()); 104 + webServer.begin(&atproto, atproto.getDid(), atproto.getHandle()); 105 105 106 106 // Load saved goal or select first available 107 107 loadOrSelectGoal(); ··· 116 116 } 117 117 } else { 118 118 // Start web server anyway to show error 119 - webServer.begin(&atproto, ""); 119 + webServer.begin(&atproto, "", ""); 120 120 webServer.setConnectionError("Failed to connect to ATProto. Check credentials."); 121 121 } 122 122
+6
firmware/esp32/GoalsGardenSync/atproto_client.cpp
··· 36 36 37 37 if (!error) { 38 38 did = respDoc["did"].as<String>(); 39 + this->handle = respDoc["handle"].as<String>(); 39 40 accessJwt = respDoc["accessJwt"].as<String>(); 40 41 refreshJwt = respDoc["refreshJwt"].as<String>(); 41 42 http.end(); ··· 49 50 50 51 void ATProtoClient::destroySession() { 51 52 did = ""; 53 + handle = ""; 52 54 accessJwt = ""; 53 55 refreshJwt = ""; 54 56 } ··· 59 61 60 62 String ATProtoClient::getDid() { 61 63 return did; 64 + } 65 + 66 + String ATProtoClient::getHandle() { 67 + return handle; 62 68 } 63 69 64 70 String ATProtoClient::httpGet(const String& endpoint) {
+2
firmware/esp32/GoalsGardenSync/atproto_client.h
··· 28 28 void destroySession(); 29 29 bool isAuthenticated(); 30 30 String getDid(); 31 + String getHandle(); 31 32 32 33 // Goals 33 34 std::vector<Goal> getGoals(int year); ··· 46 47 private: 47 48 String pdsUrl; 48 49 String did; 50 + String handle; 49 51 String accessJwt; 50 52 String refreshJwt; 51 53
+6 -5
firmware/esp32/GoalsGardenSync/web_server.cpp
··· 18 18 .no-goals a{color:#e65100;font-weight:500} 19 19 .goal{background:#fff;padding:16px;margin:8px 0;border-radius:8px;border:1px solid #ddd;cursor:pointer} 20 20 .goal:hover{border-color:#999} 21 - .goal.selected{background:#e8f5e9;border-color:#4caf50} 21 + .goal.saved{background:#e8f5e9;border-color:#4caf50} 22 22 .goal input{margin-right:12px} 23 23 .goal label{display:flex;align-items:flex-start;cursor:pointer} 24 24 .goal-info{flex:1} 25 25 .goal-name{font-weight:500} 26 26 .goal-desc{color:#666;font-size:0.9em;margin-top:4px;display:none} 27 - .goal.selected .goal-desc{display:block} 27 + .goal:has(input:checked) .goal-desc{display:block} 28 28 button{background:#4caf50;color:#fff;border:none;padding:14px 28px;border-radius:8px;font-size:1em;cursor:pointer;width:100%;margin-top:16px} 29 29 button:hover{background:#43a047} 30 30 button:disabled{background:#ccc;cursor:not-allowed} ··· 35 35 36 36 static const char HTML_FOOTER[] PROGMEM = R"rawliteral(</body></html>)rawliteral"; 37 37 38 - void GoalsWebServer::begin(ATProtoClient* atproto, const String& did) { 38 + void GoalsWebServer::begin(ATProtoClient* atproto, const String& did, const String& handle) { 39 39 atprotoClient = atproto; 40 40 userDid = did; 41 + userHandle = handle; 41 42 42 43 server.on("/", HTTP_GET, [this]() { handleRoot(); }); 43 44 server.on("/select-goal", HTTP_POST, [this]() { handleSelectGoal(); }); ··· 115 116 html += F("<p class=\"user\">Logged in as <a href=\"https://goals.garden/"); 116 117 html += escapeHtml(userDid); 117 118 html += F("\" target=\"_blank\">"); 118 - html += escapeHtml(userDid); 119 + html += escapeHtml(userHandle); 119 120 html += F("</a></p>"); 120 121 121 122 // Connection error ··· 145 146 for (const auto& goal : cachedGoals) { 146 147 bool isSelected = (goal.uri == currentGoalUri); 147 148 html += F("<div class=\"goal"); 148 - if (isSelected) html += F(" selected"); 149 + if (isSelected) html += F(" saved"); 149 150 html += F("\">"); 150 151 html += F("<label><input type=\"radio\" name=\"goal_uri\" value=\""); 151 152 html += escapeHtml(goal.uri);
+2 -1
firmware/esp32/GoalsGardenSync/web_server.h
··· 8 8 9 9 class GoalsWebServer { 10 10 public: 11 - void begin(ATProtoClient* atproto, const String& userDid); 11 + void begin(ATProtoClient* atproto, const String& userDid, const String& userHandle); 12 12 void update(); 13 13 14 14 // Goal switch request (checked from main loop) ··· 26 26 WebServer server{80}; 27 27 ATProtoClient* atprotoClient = nullptr; 28 28 String userDid; 29 + String userHandle; 29 30 30 31 // Current state 31 32 String currentGoalUri;