Add three new automations: FreshRSS digest, birthday tracker, home stack monitor
- FreshRSS Smart Digest: Daily AI-ranked RSS summary at 7 AM - Birthday Tracker: Smart reminders for family birthdays with gift suggestions - Home Stack Monitor: Health checks every 15 min with self-healing attempts All cron jobs configured and ready to run. Telegram bot token saved to .env
This commit is contained in:
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Secrets and Environment Files
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
.env.*
|
||||||
|
!/.env.example
|
||||||
|
|
||||||
|
# Credentials
|
||||||
|
credentials/
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.crt
|
||||||
|
|
||||||
|
# Runtime State (auto-generated)
|
||||||
|
*.json.state
|
||||||
|
reminder-log.json
|
||||||
|
monitor-state.json
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Node modules (if any scripts use npm)
|
||||||
|
node_modules/
|
||||||
163
automations/README.md
Normal file
163
automations/README.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Automations README
|
||||||
|
|
||||||
|
Three new automations ready to deploy! 🦀
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
**Already set up!** ✅ All cron jobs are configured and running.
|
||||||
|
|
||||||
|
Your Telegram bot token is saved in `automations/.env`. The jobs will:
|
||||||
|
- Send you Telegram notifications directly
|
||||||
|
- Run via OpenClaw's cron system (spawns lightweight sub-agents)
|
||||||
|
|
||||||
|
**To verify:**
|
||||||
|
```bash
|
||||||
|
openclaw cron list
|
||||||
|
```
|
||||||
|
|
||||||
|
**Active schedules:**
|
||||||
|
| Job | Time | What it does |
|
||||||
|
|-----|------|--------------|
|
||||||
|
| FreshRSS Daily Digest | 7:00 AM | AI-ranked RSS summary |
|
||||||
|
| Home Stack Daily Report | 8:00 AM | Service health report |
|
||||||
|
| Birthday Tracker | 9:00 AM | Birthday reminder check |
|
||||||
|
| Home Stack Monitor | Every 15 min | Health checks + alerts |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📰 FreshRSS Smart Digest
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- Pulls unread articles from your FreshRSS
|
||||||
|
- Ranks by relevance to your interests (AI, politics, EVs, LGBTQ, Perth/WA news)
|
||||||
|
- Categorizes as "Must Read" / "Skimmable"
|
||||||
|
- Delivers via Telegram at 7:00 AM
|
||||||
|
|
||||||
|
**Location:** `automations/freshrss-digest/`
|
||||||
|
|
||||||
|
**To customize interests:** Edit the `INTERESTS` array in `daily-digest.sh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎂 Birthday & Gift Tracker
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- Tracks family birthdays with smart reminders
|
||||||
|
- 2 weeks before: "Start thinking about gifts"
|
||||||
|
- 1 week before: "Time to plan something"
|
||||||
|
- Day of: "Don't forget to call!" + gift suggestions
|
||||||
|
- Logs past gifts to avoid repeats
|
||||||
|
|
||||||
|
**Pre-loaded with:**
|
||||||
|
- Grace (Mum) - June 2
|
||||||
|
- Harvey (Dad) - Dec 8
|
||||||
|
- Elizabeth (Sister) - Sept 11
|
||||||
|
- Alexander (Godson) - July
|
||||||
|
- Mia (Doggo) 🐕
|
||||||
|
|
||||||
|
**Location:** `automations/birthday-tracker/`
|
||||||
|
|
||||||
|
**Manual commands:**
|
||||||
|
```bash
|
||||||
|
# Add new person
|
||||||
|
./birthday-tracker.sh add "Friend Name" "Friend" "MM-DD" "Notes"
|
||||||
|
|
||||||
|
# Log a gift you gave
|
||||||
|
./birthday-tracker.sh gift "Grace Martin" "Flower subscription" 2025
|
||||||
|
|
||||||
|
# List all birthdays
|
||||||
|
./birthday-tracker.sh list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Home Stack Monitor
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- Checks Gitea, n8n, Home Assistant, FreshRSS every 15 minutes
|
||||||
|
- Alerts via Telegram + Gotify when services go down
|
||||||
|
- Attempts auto-recovery (where possible)
|
||||||
|
- Daily 8 AM health report with uptime stats
|
||||||
|
- Cooldowns prevent spam (1 hour between alerts)
|
||||||
|
|
||||||
|
**Location:** `automations/home-stack-monitor/`
|
||||||
|
|
||||||
|
**Manual commands:**
|
||||||
|
```bash
|
||||||
|
# Force a check now
|
||||||
|
./monitor.sh check
|
||||||
|
|
||||||
|
# Generate report
|
||||||
|
./monitor.sh report
|
||||||
|
|
||||||
|
# View status
|
||||||
|
./monitor.sh status
|
||||||
|
|
||||||
|
# Reset stats
|
||||||
|
./monitor.sh reset-stats
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 How It Works
|
||||||
|
|
||||||
|
**Cron Job Flow:**
|
||||||
|
```
|
||||||
|
OpenClaw Cron → Spawns sub-agent → Runs bash script → Sends you Telegram message
|
||||||
|
```
|
||||||
|
|
||||||
|
Each automation is a standalone bash script that:
|
||||||
|
1. Runs independently (no LLM cost for execution)
|
||||||
|
2. Sends Telegram notifications directly
|
||||||
|
3. Stores state in local JSON files
|
||||||
|
4. Gets triggered by OpenClaw's cron system
|
||||||
|
|
||||||
|
**Why sub-agents?**
|
||||||
|
OpenClaw's cron system spawns isolated sessions to run the scripts. This gives you:
|
||||||
|
- Clean execution environment
|
||||||
|
- Error isolation (one failing doesn't break others)
|
||||||
|
- Automatic logging via OpenClaw's system
|
||||||
|
|
||||||
|
**The notifications come straight to YOU** — just reply to me (Krilly) if you want to adjust anything!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔮 Future Enhancements
|
||||||
|
|
||||||
|
Ideas for when you're ready:
|
||||||
|
|
||||||
|
1. **FreshRSS + AI Briefing Merge:** Combine both into one super morning digest
|
||||||
|
2. **Smart Home Integration:** Trigger HA scenes based on calendar/mood
|
||||||
|
3. **Proxmox SSH:** Enable container restart for true self-healing
|
||||||
|
4. **Gift Shopping:** Auto-search Amazon/Kogan when birthdays approach
|
||||||
|
5. **Social CRM:** Track last contact with friends, suggest catch-ups
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- All state files are in JSON format and stored alongside scripts
|
||||||
|
- Logs use `.gitignore` patterns to avoid committing secrets
|
||||||
|
- Each script is standalone - can run manually or via cron
|
||||||
|
- Alert cooldowns prevent notification spam
|
||||||
|
|
||||||
|
**Need help?** Just ask Krilly! 🦀
|
||||||
|
|
||||||
|
Ideas for when you're ready:
|
||||||
|
|
||||||
|
1. **FreshRSS + AI Briefing Merge:** Combine both into one super morning digest
|
||||||
|
2. **Smart Home Integration:** Trigger HA scenes based on calendar/mood
|
||||||
|
3. **Proxmox SSH:** Enable container restart for true self-healing
|
||||||
|
4. **Gift Shopping:** Auto-search Amazon/Kogan when birthdays approach
|
||||||
|
5. **Social CRM:** Track last contact with friends, suggest catch-ups
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- All state files are in JSON format and stored alongside scripts
|
||||||
|
- Logs use `.gitignore` patterns to avoid committing secrets
|
||||||
|
- Each script is standalone - can run manually or via cron
|
||||||
|
- Alert cooldowns prevent notification spam
|
||||||
|
|
||||||
|
**Need help?** Just ask Krilly! 🦀
|
||||||
322
automations/birthday-tracker/birthday-tracker.sh
Executable file
322
automations/birthday-tracker/birthday-tracker.sh
Executable file
@@ -0,0 +1,322 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Birthday & Gift Tracker
|
||||||
|
# Tracks family birthdays, sends reminders, suggests gifts
|
||||||
|
# Checks daily at 9:00 AM
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DATA_FILE="$SCRIPT_DIR/birthdays.json"
|
||||||
|
LOG_FILE="$SCRIPT_DIR/reminder-log.json"
|
||||||
|
source "$SCRIPT_DIR/../../.env" 2>/dev/null || true
|
||||||
|
|
||||||
|
TELEGRAM_CHAT="${TELEGRAM_CHAT:-1793951355}"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize data file if not exists
|
||||||
|
init_data() {
|
||||||
|
if [[ ! -f "$DATA_FILE" ]]; then
|
||||||
|
log "Creating birthday database..."
|
||||||
|
cat > "$DATA_FILE" << 'EOF'
|
||||||
|
{
|
||||||
|
"people": [
|
||||||
|
{
|
||||||
|
"name": "Grace Martin",
|
||||||
|
"relationship": "Mum",
|
||||||
|
"birthday": "06-02",
|
||||||
|
"birth_year": 1951,
|
||||||
|
"notes": "Cancer treatment ongoing - be extra thoughtful. Loves gardening, cooking, family time.",
|
||||||
|
"gift_ideas": ["Flowers", "Gardening supplies", "Photo album", "Day spa voucher", "Home-cooked meal"],
|
||||||
|
"past_gifts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Harvey Martin",
|
||||||
|
"relationship": "Dad",
|
||||||
|
"birthday": "12-08",
|
||||||
|
"birth_year": 1949,
|
||||||
|
"notes": "Full-time carer for Grace. Needs respite/support. Loves tech gadgets, wine, coffee.",
|
||||||
|
"gift_ideas": ["Ember mug", "Wine subscription", "Coffee beans", "Tech gadget", "Day out voucher"],
|
||||||
|
"past_gifts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Elizabeth Martin",
|
||||||
|
"relationship": "Sister",
|
||||||
|
"birthday": "09-11",
|
||||||
|
"birth_year": 1990,
|
||||||
|
"notes": "Vegan - avoid food gifts unless specifically vegan. Creative, environmentally conscious.",
|
||||||
|
"gift_ideas": ["Vegan cookbook", "Eco-friendly products", "Plants", "Art supplies", "Experience gift"],
|
||||||
|
"past_gifts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alexander",
|
||||||
|
"relationship": "Godson/Cousin",
|
||||||
|
"birthday": "07-XX",
|
||||||
|
"birth_year": 2016,
|
||||||
|
"notes": "8 years old (born July 2016). Loves games, books, LEGO, sports.",
|
||||||
|
"gift_ideas": ["LEGO set", "Books", "Board games", "Sports equipment", "Science kit"],
|
||||||
|
"past_gifts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mia Martin",
|
||||||
|
"relationship": "Doggo 🐕",
|
||||||
|
"birthday": "XX-XX",
|
||||||
|
"notes": "14 years old, beloved geriatric girl. Treats, toys, comfy beds.",
|
||||||
|
"gift_ideas": ["Premium dog treats", "Comfy bed", "New toy", "Grooming session"],
|
||||||
|
"past_gifts": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"reminder_weeks_before": 2,
|
||||||
|
"reminder_days_before": 7,
|
||||||
|
"reminder_day_of": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$LOG_FILE" ]]; then
|
||||||
|
echo '{"sent_reminders": []}' > "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get today's date in MM-DD format
|
||||||
|
today_mmdd() {
|
||||||
|
date +%m-%d
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get today's date in YYYY-MM-DD format
|
||||||
|
today_yyyymmdd() {
|
||||||
|
date +%Y-%m-%d
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate days until birthday
|
||||||
|
days_until_birthday() {
|
||||||
|
local bday_mmdd="$1"
|
||||||
|
local today=$(today_yyyymmdd)
|
||||||
|
local current_year=$(date +%Y)
|
||||||
|
|
||||||
|
# Handle missing day (XX)
|
||||||
|
if [[ "$bday_mmdd" == *"XX"* ]]; then
|
||||||
|
echo "unknown"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local bday_this_year="$current_year-$bday_mmdd"
|
||||||
|
local bday_ts=$(date -d "$bday_this_year" +%s 2>/dev/null || echo "0")
|
||||||
|
local today_ts=$(date -d "$today" +%s)
|
||||||
|
|
||||||
|
# If birthday passed this year, calculate for next year
|
||||||
|
if ((bday_ts < today_ts)); then
|
||||||
|
((current_year++))
|
||||||
|
bday_this_year="$current_year-$bday_mmdd"
|
||||||
|
bday_ts=$(date -d "$bday_this_year" +%s)
|
||||||
|
fi
|
||||||
|
|
||||||
|
local diff=$(( (bday_ts - today_ts) / 86400 ))
|
||||||
|
echo "$diff"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if reminder already sent today
|
||||||
|
reminder_sent() {
|
||||||
|
local person="$1"
|
||||||
|
local type="$2"
|
||||||
|
local today=$(today_yyyymmdd)
|
||||||
|
|
||||||
|
grep -q "\"$today-$person-$type\"" "$LOG_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log that reminder was sent
|
||||||
|
log_reminder() {
|
||||||
|
local person="$1"
|
||||||
|
local type="$2"
|
||||||
|
local today=$(today_yyyymmdd)
|
||||||
|
|
||||||
|
# Add to log
|
||||||
|
local temp_file=$(mktemp)
|
||||||
|
jq --arg key "$today-$person-$type" '.sent_reminders += [$key]' "$LOG_FILE" > "$temp_file"
|
||||||
|
mv "$temp_file" "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean old reminders from log (keep last 90 days)
|
||||||
|
cleanup_log() {
|
||||||
|
local cutoff=$(date -d "90 days ago" +%Y-%m-%d)
|
||||||
|
local temp_file=$(mktemp)
|
||||||
|
|
||||||
|
jq --arg cutoff "$cutoff" '.sent_reminders |= map(select(. > $cutoff or length < 10))' "$LOG_FILE" > "$temp_file" 2>/dev/null || true
|
||||||
|
mv "$temp_file" "$LOG_FILE" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send Telegram message
|
||||||
|
send_telegram() {
|
||||||
|
local message="$1"
|
||||||
|
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"chat_id\": \"$TELEGRAM_CHAT\",
|
||||||
|
\"text\": \"$message\",
|
||||||
|
\"parse_mode\": \"Markdown\"
|
||||||
|
}" > /dev/null || {
|
||||||
|
log "ERROR: Failed to send Telegram message"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate gift suggestions text
|
||||||
|
gift_suggestions() {
|
||||||
|
local ideas=$(echo "$1" | jq -r '.gift_ideas | join(", ")')
|
||||||
|
local notes=$(echo "$1" | jq -r '.notes')
|
||||||
|
local past=$(echo "$1" | jq -r '.past_gifts | if length > 0 then "Past gifts: " + join(", ") else "" end')
|
||||||
|
|
||||||
|
local text="💡 *Gift Ideas:* $ideas"
|
||||||
|
if [[ -n "$past" && "$past" != "Past gifts: " ]]; then
|
||||||
|
text+="\n🎁 $past"
|
||||||
|
fi
|
||||||
|
text+="\n📝 *Notes:* $notes"
|
||||||
|
|
||||||
|
echo "$text"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check birthdays and send reminders
|
||||||
|
check_birthdays() {
|
||||||
|
log "Checking birthdays..."
|
||||||
|
|
||||||
|
local people=$(jq -c '.people[]' "$DATA_FILE")
|
||||||
|
local settings=$(jq '.settings' "$DATA_FILE")
|
||||||
|
local weeks_before=$(echo "$settings" | jq -r '.reminder_weeks_before')
|
||||||
|
local days_before=$(echo "$settings" | jq -r '.reminder_days_before')
|
||||||
|
|
||||||
|
local reminders_sent=0
|
||||||
|
|
||||||
|
while IFS= read -r person; do
|
||||||
|
local name=$(echo "$person" | jq -r '.name')
|
||||||
|
local relationship=$(echo "$person" | jq -r '.relationship')
|
||||||
|
local birthday=$(echo "$person" | jq -r '.birthday')
|
||||||
|
local birth_year=$(echo "$person" | jq -r '.birth_year // empty')
|
||||||
|
|
||||||
|
# Skip if no specific date
|
||||||
|
if [[ "$birthday" == *"XX"* ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local days_until=$(days_until_birthday "$birthday")
|
||||||
|
|
||||||
|
# Calculate age if birth year known
|
||||||
|
local age_text=""
|
||||||
|
if [[ -n "$birth_year" && "$birth_year" != "null" ]]; then
|
||||||
|
local current_year=$(date +%Y)
|
||||||
|
local age=$((current_year - birth_year))
|
||||||
|
if ((days_until < 365)); then
|
||||||
|
age_text=" (turning $age)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check reminders
|
||||||
|
local should_remind=false
|
||||||
|
local reminder_type=""
|
||||||
|
local message=""
|
||||||
|
|
||||||
|
if ((days_until == 0)); then
|
||||||
|
reminder_type="today"
|
||||||
|
if ! reminder_sent "$name" "$reminder_type"; then
|
||||||
|
message="🎂 *Birthday Today!*\n\n*$name* — your $relationship$age_text\n\nDon't forget to call/message them! 💝"
|
||||||
|
local gifts=$(gift_suggestions "$person")
|
||||||
|
message+="\n\n$gifts"
|
||||||
|
should_remind=true
|
||||||
|
fi
|
||||||
|
elif ((days_until == days_before)); then
|
||||||
|
reminder_type="week"
|
||||||
|
if ! reminder_sent "$name" "$reminder_type"; then
|
||||||
|
message="📅 *Birthday Reminder*\n\n*$name* — your $relationship$age_text\nBirthday in *$days_until days* ($birthday)\n\nTime to plan something! 🎁"
|
||||||
|
local gifts=$(gift_suggestions "$person")
|
||||||
|
message+="\n\n$gifts"
|
||||||
|
should_remind=true
|
||||||
|
fi
|
||||||
|
elif ((days_until == weeks_before * 7)); then
|
||||||
|
reminder_type="twoweeks"
|
||||||
|
if ! reminder_sent "$name" "$reminder_type"; then
|
||||||
|
message="📅 *Upcoming Birthday*\n\n*$name* — your $relationship$age_text\nBirthday in *$days_until days* ($birthday)\n\nStart thinking about gifts! 🎁"
|
||||||
|
local gifts=$(gift_suggestions "$person")
|
||||||
|
message+="\n\n$gifts"
|
||||||
|
should_remind=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$should_remind" == true ]]; then
|
||||||
|
log "Sending $reminder_type reminder for $name"
|
||||||
|
send_telegram "$message"
|
||||||
|
log_reminder "$name" "$reminder_type"
|
||||||
|
((reminders_sent++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
done <<< "$people"
|
||||||
|
|
||||||
|
log "Sent $reminders_sent reminders"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add/update a person (can be called manually)
|
||||||
|
add_person() {
|
||||||
|
local name="$1"
|
||||||
|
local relationship="$2"
|
||||||
|
local birthday="$3" # MM-DD format
|
||||||
|
local notes="$4"
|
||||||
|
|
||||||
|
local temp_file=$(mktemp)
|
||||||
|
jq --arg name "$name" \
|
||||||
|
--arg rel "$relationship" \
|
||||||
|
--arg bday "$birthday" \
|
||||||
|
--arg notes "$notes" \
|
||||||
|
'.people += [{"name": $name, "relationship": $rel, "birthday": $bday, "notes": $notes, "gift_ideas": [], "past_gifts": []}]' \
|
||||||
|
"$DATA_FILE" > "$temp_file"
|
||||||
|
mv "$temp_file" "$DATA_FILE"
|
||||||
|
|
||||||
|
log "Added $name to birthday tracker"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log a past gift
|
||||||
|
log_gift() {
|
||||||
|
local name="$1"
|
||||||
|
local gift="$2"
|
||||||
|
local year="${3:-$(date +%Y)}"
|
||||||
|
|
||||||
|
local temp_file=$(mktemp)
|
||||||
|
jq --arg name "$name" \
|
||||||
|
--arg gift "$gift ($year)" \
|
||||||
|
'(.people[] | select(.name == $name)).past_gifts += [$gift]' \
|
||||||
|
"$DATA_FILE" > "$temp_file"
|
||||||
|
mv "$temp_file" "$DATA_FILE"
|
||||||
|
|
||||||
|
log "Logged gift for $name: $gift"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
main() {
|
||||||
|
case "${1:-check}" in
|
||||||
|
check)
|
||||||
|
init_data
|
||||||
|
check_birthdays
|
||||||
|
cleanup_log
|
||||||
|
;;
|
||||||
|
add)
|
||||||
|
add_person "$2" "$3" "$4" "$5"
|
||||||
|
;;
|
||||||
|
gift)
|
||||||
|
log_gift "$2" "$3" "$4"
|
||||||
|
;;
|
||||||
|
list)
|
||||||
|
jq '.people[] | {name, relationship, birthday, notes}' "$DATA_FILE"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 [check|add|gift|list]"
|
||||||
|
echo " check - Run daily birthday check"
|
||||||
|
echo " add 'Name' 'Relationship' 'MM-DD' 'Notes' - Add new person"
|
||||||
|
echo " gift 'Name' 'Gift description' [year] - Log a gift given"
|
||||||
|
echo " list - Show all tracked birthdays"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
53
automations/birthday-tracker/birthdays.json
Normal file
53
automations/birthday-tracker/birthdays.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"people": [
|
||||||
|
{
|
||||||
|
"name": "Grace Martin",
|
||||||
|
"relationship": "Mum",
|
||||||
|
"birthday": "06-02",
|
||||||
|
"birth_year": 1951,
|
||||||
|
"notes": "Cancer treatment ongoing - be extra thoughtful. Loves gardening, cooking, family time.",
|
||||||
|
"gift_ideas": ["Flowers", "Gardening supplies", "Photo album", "Day spa voucher", "Home-cooked meal"],
|
||||||
|
"past_gifts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Harvey Martin",
|
||||||
|
"relationship": "Dad",
|
||||||
|
"birthday": "12-08",
|
||||||
|
"birth_year": 1949,
|
||||||
|
"notes": "Full-time carer for Grace. Needs respite/support. Loves tech gadgets, wine, coffee.",
|
||||||
|
"gift_ideas": ["Ember mug", "Wine subscription", "Coffee beans", "Tech gadget", "Day out voucher"],
|
||||||
|
"past_gifts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Elizabeth Martin",
|
||||||
|
"relationship": "Sister",
|
||||||
|
"birthday": "09-11",
|
||||||
|
"birth_year": 1990,
|
||||||
|
"notes": "Vegan - avoid food gifts unless specifically vegan. Creative, environmentally conscious.",
|
||||||
|
"gift_ideas": ["Vegan cookbook", "Eco-friendly products", "Plants", "Art supplies", "Experience gift"],
|
||||||
|
"past_gifts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alexander",
|
||||||
|
"relationship": "Godson/Cousin",
|
||||||
|
"birthday": "07-XX",
|
||||||
|
"birth_year": 2016,
|
||||||
|
"notes": "8 years old (born July 2016). Loves games, books, LEGO, sports.",
|
||||||
|
"gift_ideas": ["LEGO set", "Books", "Board games", "Sports equipment", "Science kit"],
|
||||||
|
"past_gifts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mia Martin",
|
||||||
|
"relationship": "Doggo 🐕",
|
||||||
|
"birthday": "XX-XX",
|
||||||
|
"notes": "14 years old, beloved geriatric girl. Treats, toys, comfy beds.",
|
||||||
|
"gift_ideas": ["Premium dog treats", "Comfy bed", "New toy", "Grooming session"],
|
||||||
|
"past_gifts": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"reminder_weeks_before": 2,
|
||||||
|
"reminder_days_before": 7,
|
||||||
|
"reminder_day_of": true
|
||||||
|
}
|
||||||
|
}
|
||||||
34
automations/cron-setup.txt
Normal file
34
automations/cron-setup.txt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# OpenClaw Automations Cron Setup
|
||||||
|
# Add these via: openclaw cron add --name "..." --schedule "..." --exec "..."
|
||||||
|
|
||||||
|
# === FRESHRSS SMART DIGEST ===
|
||||||
|
# Daily at 7:00 AM - AI-ranked RSS summary
|
||||||
|
# openclaw cron add \
|
||||||
|
# --name "FreshRSS Daily Digest" \
|
||||||
|
# --schedule "0 7 * * *" \
|
||||||
|
# --exec "/home/openclaw/.openclaw/workspace/automations/freshrss-digest/daily-digest.sh"
|
||||||
|
|
||||||
|
# === BIRTHDAY TRACKER ===
|
||||||
|
# Daily at 9:00 AM - Check for upcoming birthdays
|
||||||
|
# openclaw cron add \
|
||||||
|
# --name "Birthday Tracker Check" \
|
||||||
|
# --schedule "0 9 * * *" \
|
||||||
|
# --exec "/home/openclaw/.openclaw/workspace/automations/birthday-tracker/birthday-tracker.sh check"
|
||||||
|
|
||||||
|
# === HOME STACK MONITOR ===
|
||||||
|
# Every 15 minutes - Health checks + self-healing
|
||||||
|
# openclaw cron add \
|
||||||
|
# --name "Home Stack Monitor" \
|
||||||
|
# --schedule "*/15 * * * *" \
|
||||||
|
# --exec "/home/openclaw/.openclaw/workspace/automations/home-stack-monitor/monitor.sh check"
|
||||||
|
|
||||||
|
# === HOME STACK DAILY REPORT ===
|
||||||
|
# Daily at 8:00 AM - Summary of day's health
|
||||||
|
# openclaw cron add \
|
||||||
|
# --name "Home Stack Daily Report" \
|
||||||
|
# --schedule "0 8 * * *" \
|
||||||
|
# --exec "/home/openclaw/.openclaw/workspace/automations/home-stack-monitor/monitor.sh report"
|
||||||
|
|
||||||
|
# === WEEKLY BRIEFING (Existing) ===
|
||||||
|
# Weekend AI news digest
|
||||||
|
# Already configured separately
|
||||||
207
automations/freshrss-digest/daily-digest.sh
Executable file
207
automations/freshrss-digest/daily-digest.sh
Executable file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# FreshRSS Smart Digest
|
||||||
|
# Pulls unread articles, ranks them by relevance, delivers categorized summary
|
||||||
|
# Runs daily at 7:00 AM (before AI newsletter digest)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../.env" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Config
|
||||||
|
FRESHRSS_URL="${FRESHRSS_URL:-http://freshrss.kangaroo-eel.ts.net}"
|
||||||
|
FRESHRSS_USER="${FRESHRSS_USER:-anthony}"
|
||||||
|
TELEGRAM_CHAT="${TELEGRAM_CHAT:-1793951355}"
|
||||||
|
|
||||||
|
# Your interest keywords for relevance ranking
|
||||||
|
INTERESTS=(
|
||||||
|
"AI" "artificial intelligence" "machine learning" "LLM"
|
||||||
|
"renewable energy" "solar" "wind" "battery" "EV" "electric vehicle"
|
||||||
|
"politics" "Labor" "election" "government"
|
||||||
|
"LGBTQ" "LGBT" "queer" "transgender"
|
||||||
|
"Perth" "Western Australia" "WA"
|
||||||
|
"climate" "environment"
|
||||||
|
"mental health" "depression" "therapy"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Priority sources (always include these)
|
||||||
|
PRIORITY_SOURCES=(
|
||||||
|
"CNN"
|
||||||
|
"MSNBC"
|
||||||
|
"Al Jazeera"
|
||||||
|
"ABC News"
|
||||||
|
"The Guardian"
|
||||||
|
"BlueSky"
|
||||||
|
)
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_unread() {
|
||||||
|
log "Fetching unread articles from FreshRSS..."
|
||||||
|
|
||||||
|
local raw_output
|
||||||
|
raw_output=$(/home/openclaw/.openclaw/workspace/skills/freshrss-reader/scripts/freshrss.sh headlines \
|
||||||
|
--unread --count 50 2>/dev/null) || {
|
||||||
|
log "ERROR: Failed to fetch from FreshRSS"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "$raw_output"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Score an article based on interest keywords
|
||||||
|
score_article() {
|
||||||
|
local title="$1"
|
||||||
|
local source="$2"
|
||||||
|
local categories="$3"
|
||||||
|
local score=0
|
||||||
|
local text="${title,,} ${categories,,}"
|
||||||
|
|
||||||
|
# Check interest keywords
|
||||||
|
for interest in "${INTERESTS[@]}"; do
|
||||||
|
if [[ "$text" == *"${interest,,}"* ]]; then
|
||||||
|
((score+=2))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Priority sources get a boost
|
||||||
|
for priority in "${PRIORITY_SOURCES[@]}"; do
|
||||||
|
if [[ "$source" == *"$priority"* ]]; then
|
||||||
|
((score+=3))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "$score"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse articles and build digest
|
||||||
|
build_digest() {
|
||||||
|
local raw="$1"
|
||||||
|
local output=""
|
||||||
|
local must_read=""
|
||||||
|
local skimmable=""
|
||||||
|
local total_count=0
|
||||||
|
|
||||||
|
# Parse the output - each article is 3 lines: date/source/title, URL, categories
|
||||||
|
local current_date=""
|
||||||
|
local current_source=""
|
||||||
|
local current_title=""
|
||||||
|
local current_url=""
|
||||||
|
local current_cats=""
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Skip empty lines
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
|
||||||
|
# Check if this is a title line (starts with [)
|
||||||
|
if [[ "$line" =~ ^\[.*\].*:.*$ ]]; then
|
||||||
|
# Save previous article if exists
|
||||||
|
if [[ -n "$current_title" ]]; then
|
||||||
|
local score
|
||||||
|
score=$(score_article "$current_title" "$current_source" "$current_cats")
|
||||||
|
|
||||||
|
local article_formatted="• *${current_source}*: ${current_title}\n ${current_url}\n"
|
||||||
|
|
||||||
|
if ((score >= 5)); then
|
||||||
|
must_read+="$article_formatted\n"
|
||||||
|
elif ((score >= 2)); then
|
||||||
|
skimmable+="$article_formatted\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
((total_count++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse new article
|
||||||
|
current_date=$(echo "$line" | sed 's/^\[\([^]]*\)\].*/\1/')
|
||||||
|
current_source=$(echo "$line" | sed 's/^\[[^]]*\] \([^:]*\):.*/\1/')
|
||||||
|
current_title=$(echo "$line" | sed 's/^\[[^]]*\] [^:]*: //')
|
||||||
|
current_url=""
|
||||||
|
current_cats=""
|
||||||
|
|
||||||
|
# Check if this is a URL line
|
||||||
|
elif [[ "$line" =~ ^https?:// ]]; then
|
||||||
|
current_url="$line"
|
||||||
|
|
||||||
|
# Check if this is categories line
|
||||||
|
elif [[ "$line" =~ ^Categories:\ ]]; then
|
||||||
|
current_cats=$(echo "$line" | sed 's/Categories: //')
|
||||||
|
fi
|
||||||
|
done <<< "$raw"
|
||||||
|
|
||||||
|
# Don't forget the last article
|
||||||
|
if [[ -n "$current_title" ]]; then
|
||||||
|
local score
|
||||||
|
score=$(score_article "$current_title" "$current_source" "$current_cats")
|
||||||
|
|
||||||
|
local article_formatted="• *${current_source}*: ${current_title}\n ${current_url}\n"
|
||||||
|
|
||||||
|
if ((score >= 5)); then
|
||||||
|
must_read+="$article_formatted\n"
|
||||||
|
elif ((score >= 2)); then
|
||||||
|
skimmable+="$article_formatted\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
((total_count++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build final message
|
||||||
|
output="📰 *FreshRSS Daily Digest*\n\n"
|
||||||
|
output+="Found *$total_count* unread articles.\n\n"
|
||||||
|
|
||||||
|
if [[ -n "$must_read" ]]; then
|
||||||
|
output+="🔥 *Must Read*\n$must_read\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$skimmable" ]]; then
|
||||||
|
output+="📎 *Skimmable*\n$skimmable\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$must_read" && -z "$skimmable" ]]; then
|
||||||
|
output+="No high-priority articles today. You're caught up! 🎉\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
output+="\n📖 [Open FreshRSS]($FRESHRSS_URL)"
|
||||||
|
|
||||||
|
echo -e "$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
send_telegram() {
|
||||||
|
local message="$1"
|
||||||
|
|
||||||
|
log "Sending to Telegram..."
|
||||||
|
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"chat_id\": \"$TELEGRAM_CHAT\",
|
||||||
|
\"text\": \"$message\",
|
||||||
|
\"parse_mode\": \"Markdown\",
|
||||||
|
\"disable_web_page_preview\": true
|
||||||
|
}" > /dev/null || {
|
||||||
|
log "ERROR: Failed to send Telegram message"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log "Sent to Telegram successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
log "Starting FreshRSS digest..."
|
||||||
|
|
||||||
|
# Fetch articles
|
||||||
|
local raw_articles
|
||||||
|
raw_articles=$(fetch_unread) || exit 1
|
||||||
|
|
||||||
|
# Build digest
|
||||||
|
local digest
|
||||||
|
digest=$(build_digest "$raw_articles")
|
||||||
|
|
||||||
|
# Send
|
||||||
|
send_telegram "$digest"
|
||||||
|
|
||||||
|
log "Digest complete!"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
339
automations/home-stack-monitor/monitor.sh
Executable file
339
automations/home-stack-monitor/monitor.sh
Executable file
@@ -0,0 +1,339 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Home Stack Monitor & Self-Healing
|
||||||
|
# Monitors services, alerts on issues, attempts auto-recovery
|
||||||
|
# Runs every 15 minutes
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DATA_FILE="$SCRIPT_DIR/monitor-state.json"
|
||||||
|
source "$SCRIPT_DIR/../../.env" 2>/dev/null || true
|
||||||
|
|
||||||
|
TELEGRAM_CHAT="${TELEGRAM_CHAT:-1793951355}"
|
||||||
|
GOTIFY_URL="${GOTIFY_URL:-http://runtipi.kangaroo-eel.ts.net:8129}"
|
||||||
|
GOTIFY_TOKEN="${GOTIFY_TOKEN:-AGKnHafW3FGzBlt}"
|
||||||
|
|
||||||
|
# Services to monitor
|
||||||
|
# Format: name|url|type|restart_command(optional)
|
||||||
|
# type: http, ping, port
|
||||||
|
SERVICES=(
|
||||||
|
"Gitea|http://gitea.kangaroo-eel.ts.net:3000|http"
|
||||||
|
"n8n|http://n8n.kangaroo-eel.ts.net:5678|http"
|
||||||
|
"Home Assistant|http://homeassistant.kangaroo-eel.ts.net:8123|http"
|
||||||
|
"FreshRSS|http://freshrss.kangaroo-eel.ts.net|http"
|
||||||
|
"Tailscale|100.100.100.100|ping"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Thresholds
|
||||||
|
HTTP_TIMEOUT=10
|
||||||
|
PING_COUNT=3
|
||||||
|
DISK_WARNING=80 # Alert at 80% disk usage
|
||||||
|
DISK_CRITICAL=90 # Critical at 90%
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
init_state() {
|
||||||
|
if [[ ! -f "$DATA_FILE" ]]; then
|
||||||
|
echo '{"services": {}, "alerts_sent": {}, "stats": {"checks": 0, "failures": 0, "recoveries": 0}}' > "$DATA_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check HTTP endpoint
|
||||||
|
check_http() {
|
||||||
|
local url="$1"
|
||||||
|
local status
|
||||||
|
|
||||||
|
status=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$HTTP_TIMEOUT" "$url" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$status" == "200" || "$status" == "302" || "$status" == "401" ]]; then
|
||||||
|
echo "up"
|
||||||
|
else
|
||||||
|
echo "down:$status"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check ping
|
||||||
|
check_ping() {
|
||||||
|
local host="$1"
|
||||||
|
|
||||||
|
if ping -c "$PING_COUNT" -W 2 "$host" > /dev/null 2>&1; then
|
||||||
|
echo "up"
|
||||||
|
else
|
||||||
|
echo "down:timeout"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check disk space on Proxmox (if accessible)
|
||||||
|
check_disk() {
|
||||||
|
# This would need SSH access to Proxmox host
|
||||||
|
# For now, placeholder - can be extended with SSH key setup
|
||||||
|
echo "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update service state in JSON
|
||||||
|
update_state() {
|
||||||
|
local name="$1"
|
||||||
|
local status="$2"
|
||||||
|
local timestamp=$(date -Iseconds)
|
||||||
|
|
||||||
|
local temp_file=$(mktemp)
|
||||||
|
jq --arg name "$name" \
|
||||||
|
--arg status "$status" \
|
||||||
|
--arg time "$timestamp" \
|
||||||
|
'.services[$name] = {"status": $status, "last_check": $time}' \
|
||||||
|
"$DATA_FILE" > "$temp_file"
|
||||||
|
mv "$temp_file" "$DATA_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get previous state
|
||||||
|
get_previous_state() {
|
||||||
|
local name="$1"
|
||||||
|
jq -r ".services[\"$name\"].status // \"unknown\"" "$DATA_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if alert already sent (cooldown 1 hour)
|
||||||
|
alert_cooldown_active() {
|
||||||
|
local name="$1"
|
||||||
|
local alert_type="$2"
|
||||||
|
local cooldown_seconds=3600 # 1 hour
|
||||||
|
|
||||||
|
local last_alert=$(jq -r ".alerts_sent[\"$name-$alert_type\"] // 0" "$DATA_FILE")
|
||||||
|
local now=$(date +%s)
|
||||||
|
|
||||||
|
if ((last_alert > 0)); then
|
||||||
|
local diff=$((now - last_alert))
|
||||||
|
if ((diff < cooldown_seconds)); then
|
||||||
|
return 0 # Cooldown active
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 1 # No cooldown
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log alert sent
|
||||||
|
log_alert() {
|
||||||
|
local name="$1"
|
||||||
|
local alert_type="$2"
|
||||||
|
local now=$(date +%s)
|
||||||
|
|
||||||
|
local temp_file=$(mktemp)
|
||||||
|
jq --arg key "$name-$alert_type" \
|
||||||
|
--arg time "$now" \
|
||||||
|
'.alerts_sent[$key] = $time' \
|
||||||
|
"$DATA_FILE" > "$temp_file"
|
||||||
|
mv "$temp_file" "$DATA_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send Telegram alert
|
||||||
|
send_telegram() {
|
||||||
|
local message="$1"
|
||||||
|
local priority="${2:-normal}"
|
||||||
|
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"chat_id\": \"$TELEGRAM_CHAT\",
|
||||||
|
\"text\": \"$message\",
|
||||||
|
\"parse_mode\": \"Markdown\"
|
||||||
|
}" > /dev/null || log "Failed to send Telegram"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send Gotify alert
|
||||||
|
send_gotify() {
|
||||||
|
local title="$1"
|
||||||
|
local message="$2"
|
||||||
|
local priority="${3:-5}"
|
||||||
|
|
||||||
|
curl -s -X POST "${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"title\": \"$title\",
|
||||||
|
\"message\": \"$message\",
|
||||||
|
\"priority\": $priority
|
||||||
|
}" > /dev/null || log "Failed to send Gotify"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Attempt self-healing
|
||||||
|
attempt_heal() {
|
||||||
|
local name="$1"
|
||||||
|
local url="$2"
|
||||||
|
|
||||||
|
log "Attempting to heal $name..."
|
||||||
|
|
||||||
|
case "$name" in
|
||||||
|
"Home Assistant")
|
||||||
|
# Try to restart via SSH or API if configured
|
||||||
|
log "Home Assistant heal: Check if SSH available"
|
||||||
|
# Placeholder - would need HA SSH config
|
||||||
|
;;
|
||||||
|
"Gitea"|"n8n"|"FreshRSS")
|
||||||
|
# These are Docker/LXC - could restart container if SSH configured
|
||||||
|
log "$name heal: Would attempt container restart if SSH configured"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Wait and recheck
|
||||||
|
sleep 10
|
||||||
|
local recheck
|
||||||
|
recheck=$(check_http "$url")
|
||||||
|
|
||||||
|
if [[ "$recheck" == "up" ]]; then
|
||||||
|
log "✅ $name recovered after heal attempt"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log "❌ $name still down after heal attempt"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check all services
|
||||||
|
check_services() {
|
||||||
|
log "Checking services..."
|
||||||
|
|
||||||
|
local down_services=()
|
||||||
|
local recovered_services=()
|
||||||
|
local stats_changed=false
|
||||||
|
|
||||||
|
for service_def in "${SERVICES[@]}"; do
|
||||||
|
IFS='|' read -r name url check_type <<< "$service_def"
|
||||||
|
|
||||||
|
log "Checking $name ($url)..."
|
||||||
|
|
||||||
|
local current_status
|
||||||
|
case "$check_type" in
|
||||||
|
http) current_status=$(check_http "$url") ;;
|
||||||
|
ping) current_status=$(check_ping "$url") ;;
|
||||||
|
*) current_status="unknown" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local previous_status=$(get_previous_state "$name")
|
||||||
|
|
||||||
|
# Update state
|
||||||
|
update_state "$name" "$current_status"
|
||||||
|
|
||||||
|
# Track stats
|
||||||
|
local temp_file=$(mktemp)
|
||||||
|
jq '.stats.checks += 1' "$DATA_FILE" > "$temp_file"
|
||||||
|
mv "$temp_file" "$DATA_FILE"
|
||||||
|
|
||||||
|
# Analyze state change
|
||||||
|
if [[ "$current_status" == "up" ]]; then
|
||||||
|
if [[ "$previous_status" != "up" && "$previous_status" != "unknown" ]]; then
|
||||||
|
# Service recovered
|
||||||
|
recovered_services+=("$name")
|
||||||
|
send_telegram "✅ *$name* is back online! 🎉"
|
||||||
|
log_alert "$name" "recovery"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Service down
|
||||||
|
local status_code="${current_status#down:}"
|
||||||
|
|
||||||
|
if [[ "$previous_status" == "up" ]]; then
|
||||||
|
# Just went down
|
||||||
|
down_services+=("$name|$status_code")
|
||||||
|
|
||||||
|
# Try to heal
|
||||||
|
if attempt_heal "$name" "$url"; then
|
||||||
|
recovered_services+=("$name (auto-healed)")
|
||||||
|
update_state "$name" "up"
|
||||||
|
else
|
||||||
|
# Send alert
|
||||||
|
if ! alert_cooldown_active "$name" "down"; then
|
||||||
|
send_telegram "🚨 *Service Down: $name*\n\nStatus: $status_code\nURL: $url\n\nAuto-heal failed. Manual intervention may be needed."
|
||||||
|
send_gotify "Service Down: $name" "$name is down (status: $status_code)" 8
|
||||||
|
log_alert "$name" "down"
|
||||||
|
|
||||||
|
# Update failure stats
|
||||||
|
temp_file=$(mktemp)
|
||||||
|
jq '.stats.failures += 1' "$DATA_FILE" > "$temp_file"
|
||||||
|
mv "$temp_file" "$DATA_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [[ "$previous_status" != "up" ]]; then
|
||||||
|
# Still down
|
||||||
|
if ! alert_cooldown_active "$name" "still_down"; then
|
||||||
|
send_telegram "⚠️ *Still Down: $name*\n\nHas been down for a while. Might need attention."
|
||||||
|
log_alert "$name" "still_down"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log "Check complete. ${#down_services[@]} down, ${#recovered_services[@]} recovered"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate daily health report
|
||||||
|
daily_report() {
|
||||||
|
local stats=$(jq '.stats' "$DATA_FILE")
|
||||||
|
local checks=$(echo "$stats" | jq -r '.checks')
|
||||||
|
local failures=$(echo "$stats" | jq -r '.failures')
|
||||||
|
local uptime_pct=100
|
||||||
|
|
||||||
|
if ((checks > 0)); then
|
||||||
|
uptime_pct=$((100 - (failures * 100 / checks)))
|
||||||
|
fi
|
||||||
|
|
||||||
|
local report="🏠 *Home Stack Daily Report*\n\n"
|
||||||
|
report+="📊 *Uptime: ${uptime_pct}%*\n"
|
||||||
|
report+="🔍 Checks: $checks\n"
|
||||||
|
report+="❌ Failures: $failures\n\n"
|
||||||
|
|
||||||
|
report+="*Current Status:*\n"
|
||||||
|
|
||||||
|
for service_def in "${SERVICES[@]}"; do
|
||||||
|
IFS='|' read -r name url _ <<< "$service_def"
|
||||||
|
local status=$(jq -r ".services[\"$name\"].status // \"unknown\"" "$DATA_FILE")
|
||||||
|
local last_check=$(jq -r ".services[\"$name\"].last_check // \"never\"" "$DATA_FILE")
|
||||||
|
|
||||||
|
if [[ "$status" == "up" ]]; then
|
||||||
|
report+="✅ $name\n"
|
||||||
|
else
|
||||||
|
report+="❌ $name ($status)\n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
send_telegram "$report"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup old alerts (older than 24 hours)
|
||||||
|
cleanup_alerts() {
|
||||||
|
local cutoff=$(($(date +%s) - 86400))
|
||||||
|
local temp_file=$(mktemp)
|
||||||
|
|
||||||
|
jq --argjson cutoff "$cutoff" '.alerts_sent |= with_entries(select(.value > $cutoff))' "$DATA_FILE" > "$temp_file"
|
||||||
|
mv "$temp_file" "$DATA_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
main() {
|
||||||
|
init_state
|
||||||
|
|
||||||
|
case "${1:-check}" in
|
||||||
|
check)
|
||||||
|
check_services
|
||||||
|
cleanup_alerts
|
||||||
|
;;
|
||||||
|
report)
|
||||||
|
daily_report
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
jq '.' "$DATA_FILE"
|
||||||
|
;;
|
||||||
|
reset-stats)
|
||||||
|
local temp_file=$(mktemp)
|
||||||
|
jq '.stats = {"checks": 0, "failures": 0, "recoveries": 0}' "$DATA_FILE" > "$temp_file"
|
||||||
|
mv "$temp_file" "$DATA_FILE"
|
||||||
|
log "Stats reset"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 [check|report|status|reset-stats]"
|
||||||
|
echo " check - Run health check on all services"
|
||||||
|
echo " report - Generate daily status report"
|
||||||
|
echo " status - Show full state"
|
||||||
|
echo " reset-stats - Reset statistics counters"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
0
automations/morning-briefing/morning-briefing.sh
Normal file → Executable file
0
automations/morning-briefing/morning-briefing.sh
Normal file → Executable file
132
memory/2026-02-21-telegram-keyboard.md
Normal file
132
memory/2026-02-21-telegram-keyboard.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Session: 2026-02-21 01:19:59 UTC
|
||||||
|
|
||||||
|
- **Session Key**: agent:main:main
|
||||||
|
- **Session ID**: fdc5a6d2-e367-4150-9477-7e02b61d8812
|
||||||
|
- **Source**: telegram
|
||||||
|
|
||||||
|
## Conversation Summary
|
||||||
|
|
||||||
|
assistant: 🦀 **BOOM!** Committed and pushed to Gitea! 🎉
|
||||||
|
|
||||||
|
**✅ What's backed up:**
|
||||||
|
- 573 files changed, 163k+ lines added
|
||||||
|
- All your OpenWebUI memories now in MEMORY.md
|
||||||
|
- Playwright scraper scripts (for future use)
|
||||||
|
- Your backup scripts and configs
|
||||||
|
|
||||||
|
**📍 Location:** `http://gitea.kangaroo-eel.ts.net:3000/Anthony/openclaw-backups.git`
|
||||||
|
|
||||||
|
Krilly's memory banks are now fully loaded with your life story! 🦀💾
|
||||||
|
|
||||||
|
Want me to do anything with all this juicy intel? Or shall we call it a win and let the crabs sleep? 🌊
|
||||||
|
assistant: 🦀 On it! Let me commit and push to your Gitea:
|
||||||
|
assistant: 🦀 **BOOM!** Committed and pushed to Gitea! 🎉
|
||||||
|
|
||||||
|
**✅ What's backed up:**
|
||||||
|
- 573 files changed, 163k+ lines added
|
||||||
|
- All your OpenWebUI memories now in MEMORY.md
|
||||||
|
- Playwright scraper scripts (for future use)
|
||||||
|
- Your backup scripts and configs
|
||||||
|
|
||||||
|
**📍 Location:** `http://gitea.kangaroo-eel.ts.net:3000/Anthony/openclaw-backups.git`
|
||||||
|
|
||||||
|
Krilly's memory banks are now fully loaded with your life story! 🦀💾
|
||||||
|
|
||||||
|
Want me to do anything with all this juicy intel? Or shall we call it a win and let the crabs sleep? 🌊
|
||||||
|
user: Conversation info (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "1156",
|
||||||
|
"sender": "1793951355"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
New task Can you remove the custom keyboard that has approve and deny buttons from my Krillyopenclaw_bot in telegram
|
||||||
|
assistant: 🦀 Got it! Let me remove that keyboard from your bot. I'll send a message to clear it:
|
||||||
|
assistant: Keyboard cleared! 🦀
|
||||||
|
assistant: 🦀 I just cleared the keyboard from our current chat, but to remove it from **@Krillyopenclaw_bot** (your bot), I need the bot token or you can do it yourself:
|
||||||
|
|
||||||
|
**Option A: You do it** (quickest)
|
||||||
|
Send this via your bot to any chat with the stuck keyboard:
|
||||||
|
```json
|
||||||
|
{"remove_keyboard": true, "selective": false}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Give me the bot token**
|
||||||
|
If you share the bot token, I can send the keyboard removal as your bot.
|
||||||
|
|
||||||
|
**Option C: BotFather reset**
|
||||||
|
Go to @BotFather → /setcommands → Select your bot → Send empty command list. This clears persistent keyboards.
|
||||||
|
|
||||||
|
Which way do you want to scuttle? 🦀
|
||||||
|
user: [media attached: /home/openclaw/.openclaw/media/inbound/file_5---58d1aa67-3ca5-498d-82e8-9cfce0c182dd.jpg (image/jpeg) | /home/openclaw/.openclaw/media/inbound/file_5---58d1aa67-3ca5-498d-82e8-9cfce0c182dd.jpg]
|
||||||
|
To send an image back, prefer the message tool (media/path/filePath). If you must inline, use MEDIA:https://example.com/image.jpg (spaces ok, quote if needed) or a safe relative path like MEDIA:./image.jpg. Avoid absolute paths (MEDIA:/...) and ~ paths — they are blocked for security. Keep caption in the text body.
|
||||||
|
Conversation info (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "1160",
|
||||||
|
"sender": "1793951355"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is what I am talking about
|
||||||
|
assistant: 🦀 Hmm, looking at your screenshot, I see our chat but I don't actually see a custom keyboard with approve/deny buttons visible. The bottom area just looks like the normal Telegram input field (message box + send button).
|
||||||
|
|
||||||
|
A few questions:
|
||||||
|
|
||||||
|
1. **Is the keyboard still showing RIGHT NOW** in this chat? After I sent "Keyboard cleared! 🦀"?
|
||||||
|
|
||||||
|
2. **Or is this about a DIFFERENT chat** with @Krillyopenclaw_bot (not this conversation)?
|
||||||
|
|
||||||
|
3. **What exactly do you see** at the bottom - do you see two big buttons that say "Approve" and "Deny"?
|
||||||
|
|
||||||
|
The keyboard I tried to clear only affects THIS chat (where we're talking right now). If you're seeing a keyboard in a different bot chat, I need the bot token to fix it from that side.
|
||||||
|
|
||||||
|
Can you clarify which chat has the stuck keyboard? 🦀
|
||||||
|
user: [media attached: /home/openclaw/.openclaw/media/inbound/file_6---5b248b4c-5604-4fb9-a016-47155474cf67.jpg (image/jpeg) | /home/openclaw/.openclaw/media/inbound/file_6---5b248b4c-5604-4fb9-a016-47155474cf67.jpg]
|
||||||
|
To send an image back, prefer the message tool (media/path/filePath). If you must inline, use MEDIA:https://example.com/image.jpg (spaces ok, quote if needed) or a safe relative path like MEDIA:./image.jpg. Avoid absolute paths (MEDIA:/...) and ~ paths — they are blocked for security. Keep caption in the text body.
|
||||||
|
Conversation info (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "1162",
|
||||||
|
"sender": "1793951355"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<media:image>
|
||||||
|
assistant: 🦀 AH! Now I see it! "APPROVE" and "REGEN" buttons - that's an **inline keyboard** stuck at the bottom. Let me nuke it properly:
|
||||||
|
assistant: Clearing keyboard...
|
||||||
|
assistant: 🦀 Hmm, that bot token didn't work for this chat. The keyboard is being sent by **@Krillyopenclaw_bot** (a different bot), but I don't have that bot's token to clear it.
|
||||||
|
|
||||||
|
**To fix this, you need to either:**
|
||||||
|
|
||||||
|
**Option A: Give me the bot token for @Krillyopenclaw_bot**
|
||||||
|
Then I can send the keyboard removal command as that bot.
|
||||||
|
|
||||||
|
**Option B: BotFather method** (safest)
|
||||||
|
1. Go to @BotFather
|
||||||
|
2. Type `/revoke` and select @Krillyopenclaw_bot
|
||||||
|
3. This clears all active keyboards
|
||||||
|
|
||||||
|
**Option C: Restart the bot conversation**
|
||||||
|
1. Go to @Krillyopenclaw_bot chat
|
||||||
|
2. Type `/start` - this usually resets the keyboard
|
||||||
|
3. Or delete and restart the chat with the bot
|
||||||
|
|
||||||
|
The keyboard is stuck because it was sent by a different bot instance. I can only clear keyboards from THIS chat (where we're talking now), not from other bot chats.
|
||||||
|
|
||||||
|
Which option works for you? 🦀
|
||||||
|
user: Conversation info (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "1166",
|
||||||
|
"sender": "1793951355"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
8598508497:AAHmTMbnR7un2ADtmsjJr8moQkDOU9ILBps
|
||||||
|
assistant: 🦀 **DONE!** Keyboard cleared from @Krillyopenclaw_bot!
|
||||||
|
|
||||||
|
The bot just sent you a message with the keyboard removal command. Check the chat with @Krillyopenclaw_bot - those "APPROVE" and "REGEN" buttons should be GONE now! 🎉
|
||||||
|
|
||||||
|
Let me know if it's still showing and I'll try a different approach! 🦀💨
|
||||||
Reference in New Issue
Block a user