Add lock file to web monitor script to prevent concurrent runs
@ -1 +0,0 @@
|
||||
/* Basic Xcode project file for GlassTimer. Paste this into a .xcodeproj or generate via Xcode. For now, this is a placeholder—use Xcode to create the full proj from the Sources folder. */
|
||||
@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>GlassTimer</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>GlassTimer</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.example.GlassTimer</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>GlassTimer</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>26.3</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -1,22 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
TimerView()
|
||||
.tabItem {
|
||||
Label("Timer", systemImage: "timer")
|
||||
}
|
||||
|
||||
HistoryView()
|
||||
.tabItem {
|
||||
Label("History", systemImage: "clock.arrow.circlepath")
|
||||
}
|
||||
}
|
||||
.tint(.blue) // Modern tint for tab bar
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
@main
|
||||
struct GlassTimerApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.modelContainer(for: Session.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HistoryView: View {
|
||||
@Query private var sessions: [Session]
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List(sessions) { session in
|
||||
VStack(alignment: .leading) {
|
||||
Text(session.date.formatted(.dateTime.month().day().hour().minute()))
|
||||
Text("Duration: \(Int(session.duration / 60)) min")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Sessions")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Clear All") {
|
||||
sessions.forEach { modelContext.delete($0) }
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
HistoryView()
|
||||
.modelContainer(for: Session.self)
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
@MainActor
|
||||
final class Session {
|
||||
@Attribute(.unique) var id: UUID
|
||||
var duration: TimeInterval
|
||||
var completed: Bool
|
||||
var date: Date
|
||||
|
||||
init(duration: TimeInterval, completed: Bool) {
|
||||
self.id = UUID()
|
||||
self.duration = duration
|
||||
self.completed = completed
|
||||
self.date = Date()
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import Foundation
|
||||
import AlarmKit
|
||||
import OSLog
|
||||
|
||||
@MainActor
|
||||
final class TimerService {
|
||||
private let logger = Logger(subsystem: "GlassTimer", category: "Service")
|
||||
|
||||
nonisolated func scheduleAlarm(for duration: TimeInterval) async {
|
||||
do {
|
||||
let alarm = Alarm(
|
||||
title: "Time's Up!",
|
||||
date: Date().addingTimeInterval(duration),
|
||||
sound: .default
|
||||
)
|
||||
try await AlarmController.shared.schedule(alarm)
|
||||
logger.info("Alarm scheduled")
|
||||
} catch {
|
||||
logger.error("Failed to schedule alarm: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,89 +0,0 @@
|
||||
import SwiftUI
|
||||
import AlarmKit
|
||||
import Observation
|
||||
|
||||
@Observable
|
||||
@MainActor
|
||||
final class TimerStore {
|
||||
var timeRemaining: TimeInterval = 25 * 60 // 25 min Pomodoro
|
||||
var isRunning = false
|
||||
var session: Session?
|
||||
|
||||
private var timer: Task<Void, Never>?
|
||||
private let service = TimerService()
|
||||
}
|
||||
|
||||
struct TimerView: View {
|
||||
@State private var store = TimerStore()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 40) {
|
||||
Text(timeString(from: store.timeRemaining))
|
||||
.font(.system(size: 80, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
VStack(spacing: 20) {
|
||||
Button(store.isRunning ? "Pause" : "Start") {
|
||||
toggleTimer()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
|
||||
Button("Reset") {
|
||||
resetTimer()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.navigationTitle("Pomodoro Timer")
|
||||
.sheet(isPresented: .constant(store.isRunning)) {
|
||||
// Optional: Session summary sheet with AI
|
||||
Text("Timer active!")
|
||||
}
|
||||
}
|
||||
.glassEffect() // Liquid Glass for the nav stack
|
||||
}
|
||||
|
||||
private func toggleTimer() {
|
||||
if store.isRunning {
|
||||
store.timer?.cancel()
|
||||
store.timer = nil
|
||||
store.isRunning = false
|
||||
// Save session to SwiftData
|
||||
store.session = Session(duration: store.timeRemaining, completed: true)
|
||||
} else {
|
||||
store.isRunning = true
|
||||
store.timer = Task {
|
||||
while store.timeRemaining > 0 && !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
store.timeRemaining -= 1
|
||||
}
|
||||
if !Task.isCancelled {
|
||||
// Alarm via AlarmKit
|
||||
await store.service.scheduleAlarm(for: store.timeRemaining)
|
||||
store.isRunning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resetTimer() {
|
||||
store.timer?.cancel()
|
||||
store.timeRemaining = 25 * 60
|
||||
store.isRunning = false
|
||||
}
|
||||
|
||||
private func timeString(from timeInterval: TimeInterval) -> String {
|
||||
let minutes = Int(timeInterval) / 60
|
||||
let seconds = Int(timeInterval) % 60
|
||||
return String(format: "%02d:%02d", minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
TimerView()
|
||||
}
|
||||
75
daily-digest-2026-02-20.md
Normal file
@ -0,0 +1,75 @@
|
||||
## Daily Digest - February 20, 2026
|
||||
|
||||
### 🤖 iOS AI Development
|
||||
|
||||
**Apple Foundation Models Framework Now Available**
|
||||
Apple has released the Foundation Models framework giving developers direct access to the on-device foundation model at the core of Apple Intelligence. With native Swift support, you can tap into the model with as few as three lines of code to power features like text extraction, summarization, and more - all working without internet connectivity.
|
||||
[Read more →](https://developer.apple.com/machine-learning/)
|
||||
|
||||
**SpeechAnalyzer Brings Advanced On-Device Transcription to iOS**
|
||||
The all-new SpeechAnalyzer framework enables advanced, on-device transcription capabilities for your apps. Take advantage of speech recognition and saliency features for a variety of languages without sending audio data to the cloud.
|
||||
[Read more →](https://developer.apple.com/machine-learning/)
|
||||
|
||||
**Core ML Updates for Vision and Document Recognition**
|
||||
New updates to Core ML and Vision frameworks bring full-document text recognition and camera smudge detection to elevate your app's image analysis capabilities on Apple devices.
|
||||
[Read more →](https://developer.apple.com/machine-learning/)
|
||||
|
||||
### 💻 AI Coding Assistants
|
||||
|
||||
**Cursor Launches Plugin Marketplace with Partners Including Figma, Stripe, AWS**
|
||||
Cursor has introduced plugins that package skills, subagents, MCP servers, hooks, and rules into single installs. Initial partners include Amplitude, AWS, Figma, Linear, and Stripe, covering workflows across design, databases, payments, analytics, and deployment.
|
||||
[Read more →](https://cursor.com/blog/marketplace)
|
||||
|
||||
**Cursor CLI Gets Cloud Handoff and ASCII Mermaid Diagrams**
|
||||
The latest Cursor CLI release introduces the ability to hand off plans from CLI to cloud, inline rendering of ASCII diagrams from Mermaid code blocks, and improved keyboard shortcuts for plan navigation.
|
||||
[Read more →](https://cursor.com/changelog)
|
||||
|
||||
**Stripe Releases Minions - One-Shot End-to-End Coding Agents**
|
||||
Stripe has published Part 2 of their coding agents series, detailing "Minions" - their one-shot end-to-end coding agents that help automate development workflows.
|
||||
[Read more →](https://stripe.dev/blog/minions-stripes-one-shot-end-to-end-codi)
|
||||
|
||||
**GitHub Copilot Now Supports Multiple LLMs and Custom Agents**
|
||||
GitHub Copilot now lets developers choose from leading LLMs optimized for speed, accuracy, or cost. The platform supports custom agents and third-party MCP servers to extend functionality.
|
||||
[Read more →](https://github.com/features/copilot)
|
||||
|
||||
### 🧠 Latest Coding Models
|
||||
|
||||
**Claude Opus 4.6 Released with Major Coding Improvements**
|
||||
Anthropic has upgraded their smartest model - Opus 4.6 is now an industry-leading model for agentic coding, computer use, tool use, search, and finance, often winning by wide margins in benchmarks.
|
||||
[Read more →](https://www.anthropic.com/news)
|
||||
|
||||
**Gemini 3.1 Pro Rolls Out with Advanced Reasoning**
|
||||
Google's new Gemini 3.1 Pro AI model "represents a step forward in core reasoning" according to Google. The model is designed for tasks where a simple answer isn't enough, rolling out now in the Gemini app and NotebookLM.
|
||||
[Read more →](https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-1-pro/)
|
||||
|
||||
**GGML.ai Joins Hugging Face to Advance Local AI**
|
||||
GGML.ai, the organization behind llama.cpp, is joining Hugging Face to ensure the long-term progress of Local AI. This partnership strengthens the ecosystem for running AI models locally on consumer hardware.
|
||||
[Read more →](https://github.com/ggml-org/llama.cpp/discussions/19759)
|
||||
|
||||
**Taalas Demonstrates Path to 17k tokens/sec Ubiquitous AI**
|
||||
Taalas has shared research on achieving ubiquitous AI with breakthrough performance of 17,000 tokens per second, showing a potential path to making AI inference dramatically faster and more accessible.
|
||||
[Read more →](https://taalas.com/the-path-to-ubiquitous-ai/)
|
||||
|
||||
### 🦾 OpenClaw Updates
|
||||
|
||||
**OpenClaw Mentioned in Major Security Report on Cline Vulnerability**
|
||||
A hacker reportedly tricked Cline's Claude-powered workflow into installing OpenClaw on computers, highlighting the importance of verifying AI agent actions. The incident was covered by The Verge as part of broader AI security concerns.
|
||||
[Read more →](https://www.theverge.com/ai-artificial-intelligence)
|
||||
|
||||
### 🚀 Digital Entrepreneurship
|
||||
|
||||
**Bootstrapping a $20k/mo AI Portfolio After VC-Backed Failure**
|
||||
An inspiring story of an entrepreneur who built a $20,000/month AI portfolio through bootstrapping after their VC-backed company failed. The approach focuses on sustainable revenue over growth-at-all-costs.
|
||||
[Read more →](https://www.indiehackers.com/post/tech/bootstrapping-a-20k-mo-ai-portfolio-after-his-vc-backed-company-failed-rQxwZBD9xWVgfHhIxvbJ)
|
||||
|
||||
**Hitting $10k/mo by Using Agency as Testing Ground and Distribution**
|
||||
A developer shares how they reached $10,000/month by using their agency as both a testing ground for product ideas and a distribution channel for their SaaS products.
|
||||
[Read more →](https://www.indiehackers.com/post/tech/hitting-10k-mo-by-using-an-agency-as-both-testing-ground-and-distribution-FF8kooe4FWGH9sHjVrT3)
|
||||
|
||||
**Bazzly: Your SaaS Needs a Distribution Habit, Not Just Strategy**
|
||||
A new product launching with the insight that SaaS companies don't need complex marketing strategies - they need consistent distribution habits to reach customers effectively.
|
||||
[Read more →](https://www.indiehackers.com/product/bazzly)
|
||||
|
||||
**LLM Eagle: New LLM Visibility Tool for Developers**
|
||||
A new indie hacking project creating an LLM visibility tool that focuses on simplicity and developer experience, aiming to solve monitoring challenges without the complexity of enterprise solutions.
|
||||
[Read more →](https://www.indiehackers.com/product/llm-eagle)
|
||||
1
digest-payload.json
Normal file
BIN
gantt-all-projects.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
gantt-all-statuses.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
gantt-backlog-redesign.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
gantt-backlog-view.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
gantt-board-current.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
gantt-fixed-sprint.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
gantt-fixed.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
gantt-project-pills.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
gantt-simple-sidebar.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
gantt-sprint-board.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
gantt-sprint-feature.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
gantt-sprint-sections.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
gantt-test.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
gantt-workflow-columns.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
@ -21,3 +21,29 @@ Matt shared details about his life:
|
||||
- Fixed cron auto-restart script (PATH issue)
|
||||
- Installed Playwright globally for screenshots
|
||||
- Created iOS MRR opportunities research report
|
||||
|
||||
## Sprint Feature Implementation (Evening)
|
||||
|
||||
**Task #15: Sprint functionality for Gantt Board**
|
||||
|
||||
### Implemented:
|
||||
- **Two views:** Kanban (current sprint) and Backlog (grooming)
|
||||
- **Sprint 1 created:** Feb 16-22, 2026 (Mon-Sun), status: active
|
||||
- **All 13 tasks attached to Sprint 1**
|
||||
- **Kanban board:** 3 columns - To Do, In Progress, Done
|
||||
- **Project pills:** Added to task cards showing which project each task belongs to
|
||||
- **Simplified sidebar:** Shows current sprint info and project list
|
||||
- **Backlog view:** For sprint planning and grooming
|
||||
|
||||
### Technical Changes:
|
||||
- Added Sprint type and interface to store
|
||||
- Updated API to handle sprints
|
||||
- Created SprintBoard and BacklogView components
|
||||
- Integrated @dnd-kit for drag-and-drop
|
||||
- Fixed data persistence to include sprints
|
||||
|
||||
### Current State:
|
||||
- All web apps healthy (ports 3000, 3003, 3005)
|
||||
- Gantt Board live at http://localhost:3000
|
||||
- 2 view toggle: Kanban | Backlog
|
||||
- Matt reviewing and providing feedback on implementation
|
||||
|
||||
@ -103,3 +103,66 @@ No restarts required.
|
||||
[2026-02-19 12:01:40 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 12:26:40 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 12:56:39 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 13:26:40 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 14:01:39 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 14:31:39 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 15:06:42 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 15:26:40 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 15:56:39 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 16:31:39 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 17:01:39 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 17:31:39 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 18:01:40 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 18:36:41 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 18:41:39 CST] ⚠️ gantt-board (port 3000) is DOWN - restarting...
|
||||
[2026-02-19 18:41:39 CST] 🔄 gantt-board restarted on port 3000
|
||||
[2026-02-19 18:41:39 CST] ❌ gantt-board still unhealthy (HTTP 500)
|
||||
[2026-02-19 19:16:40 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 19:46:42 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 20:01:39 CST] ⚠️ gantt-board (port 3000) is DOWN - restarting...
|
||||
[2026-02-19 20:01:39 CST] 🔄 gantt-board restarted on port 3000
|
||||
[2026-02-19 20:01:39 CST] ❌ gantt-board still unhealthy (HTTP 500)
|
||||
[2026-02-19 20:06:40 CST] ⚠️ gantt-board (port 3000) is DOWN - restarting...
|
||||
[2026-02-19 20:06:40 CST] 🔄 gantt-board restarted on port 3000
|
||||
[2026-02-19 20:06:40 CST] ❌ gantt-board still unhealthy (HTTP 500)
|
||||
[2026-02-19 20:07:03 CST] ⚠️ gantt-board (port 3000) is DOWN - restarting...
|
||||
[2026-02-19 20:07:03 CST] 🔄 gantt-board restarted on port 3000
|
||||
[2026-02-19 20:07:03 CST] ❌ gantt-board still unhealthy (HTTP 500)
|
||||
[2026-02-19 20:11:40 CST] ⚠️ blog-backup (port 3003) is DOWN - restarting...
|
||||
[2026-02-19 20:11:40 CST] 🔄 blog-backup restarted on port 3003
|
||||
[2026-02-19 20:11:40 CST] ⚠️ heartbeat-monitor (port 3005) is DOWN - restarting...
|
||||
[2026-02-19 20:11:40 CST] 🔄 heartbeat-monitor restarted on port 3005
|
||||
[2026-02-19 20:11:40 CST] ❌ blog-backup still unhealthy (HTTP 000DOWN)
|
||||
[2026-02-19 20:11:40 CST] ❌ heartbeat-monitor still unhealthy (HTTP 000DOWN)
|
||||
[2026-02-19 20:16:39 CST] ⚠️ blog-backup (port 3003) is DOWN - restarting...
|
||||
[2026-02-19 20:16:39 CST] 🔄 blog-backup restarted on port 3003
|
||||
[2026-02-19 20:16:39 CST] ⚠️ heartbeat-monitor (port 3005) is DOWN - restarting...
|
||||
[2026-02-19 20:16:39 CST] 🔄 heartbeat-monitor restarted on port 3005
|
||||
[2026-02-19 20:16:39 CST] ❌ blog-backup still unhealthy (HTTP 000DOWN)
|
||||
[2026-02-19 20:16:39 CST] ❌ heartbeat-monitor still unhealthy (HTTP 000DOWN)
|
||||
[2026-02-19 20:36:40 CST] ✅ All web apps healthy (3000, 3003, 3005)
|
||||
[2026-02-19 21:19:29 CST] ⚠️ gantt-board (port 3000) is DOWN - restarting...
|
||||
[2026-02-19 21:19:29 CST] 🔄 gantt-board restarted on port 3000
|
||||
[2026-02-19 21:19:29 CST] ❌ gantt-board still unhealthy (HTTP 404)
|
||||
[2026-02-20 03:46:57 CST] ⚠️ blog-backup (port 3003) is DOWN - restarting...
|
||||
[2026-02-20 03:46:57 CST] 🔄 blog-backup restarted on port 3003
|
||||
[2026-02-20 03:46:57 CST] ⚠️ heartbeat-monitor (port 3005) is DOWN - restarting...
|
||||
[2026-02-20 03:46:57 CST] 🔄 heartbeat-monitor restarted on port 3005
|
||||
[2026-02-20 03:46:57 CST] ❌ blog-backup still unhealthy (HTTP 000DOWN)
|
||||
[2026-02-20 03:46:57 CST] ❌ heartbeat-monitor still unhealthy (HTTP 000DOWN)
|
||||
[2026-02-20 05:48:57 CST] ⚠️ blog-backup (port 3003) is DOWN - restarting...
|
||||
[2026-02-20 05:48:57 CST] 🔄 blog-backup restarted on port 3003
|
||||
[2026-02-20 05:48:57 CST] ⚠️ heartbeat-monitor (port 3005) is DOWN - restarting...
|
||||
[2026-02-20 05:48:57 CST] 🔄 heartbeat-monitor restarted on port 3005
|
||||
[2026-02-20 05:48:57 CST] ❌ blog-backup still unhealthy (HTTP 000DOWN)
|
||||
[2026-02-20 05:48:57 CST] ❌ heartbeat-monitor still unhealthy (HTTP 000DOWN)
|
||||
[2026-02-20 06:49:57 CST] ⚠️ blog-backup (port 3003) is DOWN - restarting...
|
||||
[2026-02-20 06:49:57 CST] 🔄 blog-backup restarted on port 3003
|
||||
[2026-02-20 06:49:57 CST] ⚠️ heartbeat-monitor (port 3005) is DOWN - restarting...
|
||||
[2026-02-20 06:49:57 CST] 🔄 heartbeat-monitor restarted on port 3005
|
||||
[2026-02-20 06:49:57 CST] ❌ blog-backup still unhealthy (HTTP 000DOWN)
|
||||
[2026-02-20 06:49:57 CST] ❌ heartbeat-monitor still unhealthy (HTTP 000DOWN)
|
||||
[2026-02-20 08:55:03 CST] ⚠️ blog-backup (port 3003) is DOWN - restarting...
|
||||
[2026-02-20 08:55:03 CST] 🔄 blog-backup restarted on port 3003
|
||||
[2026-02-20 08:55:03 CST] ⚠️ heartbeat-monitor (port 3005) is DOWN - restarting...
|
||||
[2026-02-20 08:55:03 CST] 🔄 heartbeat-monitor restarted on port 3005
|
||||
|
||||
BIN
proof-working.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
@ -4,8 +4,28 @@
|
||||
# Ports: 3000 (gantt-board), 3003 (blog-backup), 3005 (heartbeat-monitor)
|
||||
|
||||
LOG_FILE="/Users/mattbruce/.openclaw/workspace/memory/web-monitor.log"
|
||||
LOCK_FILE="/tmp/web-monitor.lock"
|
||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S %Z')
|
||||
|
||||
# Prevent concurrent runs
|
||||
if [ -f "$LOCK_FILE" ]; then
|
||||
# Check if lock is stale (older than 2 minutes)
|
||||
lock_age=$(($(date +%s) - $(stat -c %Y "$LOCK_FILE" 2>/dev/null || stat -f %m "$LOCK_FILE" 2>/dev/null || echo 0)))
|
||||
if [ "$lock_age" -lt 120 ]; then
|
||||
echo "[$TIMESTAMP] Monitor already running, skipping..." >> "$LOG_FILE"
|
||||
exit 0
|
||||
else
|
||||
echo "[$TIMESTAMP] Removing stale lock file" >> "$LOG_FILE"
|
||||
rm -f "$LOCK_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create lock file
|
||||
touch "$LOCK_FILE"
|
||||
|
||||
# Ensure lock is removed on exit
|
||||
trap "rm -f $LOCK_FILE" EXIT
|
||||
|
||||
# Ensure PATH for cron (include Homebrew on macOS)
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$HOME/.local/bin:$PATH"
|
||||
|
||||
|
||||