
Implements automatic resource production with database persistence: - Mining table with timestamp, value, and rate tracking - Background ticker updates values every minute - API endpoint calculates real-time values from database snapshots - Frontend displays live-updating current value and database history - Rate-based accumulation system for efficient resource management 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
424 lines
10 KiB
Go
424 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
type Item struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type UserInfo struct {
|
|
Username string `json:"username"`
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
Groups string `json:"groups"`
|
|
Headers map[string]string `json:"headers"`
|
|
}
|
|
|
|
type MiningRecord struct {
|
|
ID int `json:"id"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Value float64 `json:"value"`
|
|
Rate float64 `json:"rate"`
|
|
}
|
|
|
|
type MiningData struct {
|
|
CurrentValue float64 `json:"current_value"`
|
|
Rate float64 `json:"rate"`
|
|
History []MiningRecord `json:"history"`
|
|
}
|
|
|
|
var db *sql.DB
|
|
|
|
func initDB() {
|
|
var err error
|
|
db, err = sql.Open("sqlite", "items.db")
|
|
if err != nil {
|
|
log.Fatal("Failed to open database:", err)
|
|
}
|
|
|
|
createItemsTable := `
|
|
CREATE TABLE IF NOT EXISTS items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL
|
|
);`
|
|
|
|
_, err = db.Exec(createItemsTable)
|
|
if err != nil {
|
|
log.Fatal("Failed to create items table:", err)
|
|
}
|
|
|
|
createMiningTable := `
|
|
CREATE TABLE IF NOT EXISTS mining (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp DATETIME NOT NULL,
|
|
value REAL NOT NULL,
|
|
rate REAL NOT NULL
|
|
);`
|
|
|
|
_, err = db.Exec(createMiningTable)
|
|
if err != nil {
|
|
log.Fatal("Failed to create mining table:", err)
|
|
}
|
|
|
|
// Insert initial mining record if table is empty
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM mining").Scan(&count)
|
|
if err != nil {
|
|
log.Fatal("Failed to check mining table:", err)
|
|
}
|
|
|
|
if count == 0 {
|
|
_, err = db.Exec("INSERT INTO mining (timestamp, value, rate) VALUES (?, ?, ?)",
|
|
time.Now(), 100.0, 2.5)
|
|
if err != nil {
|
|
log.Fatal("Failed to insert initial mining record:", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) > 1 && os.Args[1] == "--watch" {
|
|
watchAndRestart()
|
|
return
|
|
}
|
|
|
|
// Initialize database
|
|
initDB()
|
|
defer db.Close()
|
|
|
|
// Start mining ticker
|
|
go startMiningTicker()
|
|
|
|
// Your actual API
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
http.HandleFunc("/randomnumber", func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, "%d", rng.Intn(1000))
|
|
})
|
|
|
|
http.HandleFunc("/slowtest", func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(1 * time.Second)
|
|
fmt.Fprintf(w, "true")
|
|
})
|
|
|
|
http.HandleFunc("/welcome", func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, "welcome to shinyspace play api testing")
|
|
})
|
|
|
|
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == "POST" {
|
|
r.ParseMultipartForm(32 << 20) // 32MB max memory
|
|
text := r.FormValue("text")
|
|
fmt.Fprintf(w, "you said %s", text)
|
|
} else {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
fmt.Fprintf(w, "Only POST method allowed")
|
|
}
|
|
})
|
|
|
|
// Items API endpoints
|
|
http.HandleFunc("/items", handleItems)
|
|
http.HandleFunc("/items/", handleItemDelete)
|
|
|
|
// Mining endpoints
|
|
http.HandleFunc("/mining", handleMining)
|
|
|
|
// Auth endpoints
|
|
http.HandleFunc("/auth/user", handleAuthUser)
|
|
|
|
log.Println("API running on :3847")
|
|
http.ListenAndServe("127.0.0.1:3847", nil)
|
|
}
|
|
|
|
func watchAndRestart() {
|
|
binaryPath, _ := os.Executable()
|
|
var cmd *exec.Cmd
|
|
|
|
start := func() {
|
|
if cmd != nil && cmd.Process != nil {
|
|
cmd.Process.Kill()
|
|
}
|
|
cmd = exec.Command(binaryPath)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Start()
|
|
}
|
|
|
|
start() // Initial start
|
|
|
|
lastMod := getModTime(binaryPath)
|
|
for {
|
|
time.Sleep(1 * time.Second)
|
|
if mod := getModTime(binaryPath); mod.After(lastMod) {
|
|
log.Println("Binary changed, restarting...")
|
|
lastMod = mod
|
|
start()
|
|
}
|
|
}
|
|
}
|
|
|
|
func getModTime(path string) time.Time {
|
|
info, _ := os.Stat(path)
|
|
return info.ModTime()
|
|
}
|
|
|
|
func handleItems(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
switch r.Method {
|
|
case "GET":
|
|
getItems(w, r)
|
|
case "POST":
|
|
createItem(w, r)
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Method not allowed"})
|
|
}
|
|
}
|
|
|
|
func getItems(w http.ResponseWriter, r *http.Request) {
|
|
rows, err := db.Query("SELECT id, name FROM items ORDER BY id")
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to fetch items"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var items []Item
|
|
for rows.Next() {
|
|
var item Item
|
|
err := rows.Scan(&item.ID, &item.Name)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to scan items"})
|
|
return
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
|
|
// Ensure we always return an array, even if empty
|
|
if items == nil {
|
|
items = []Item{}
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(items)
|
|
}
|
|
|
|
func createItem(w http.ResponseWriter, r *http.Request) {
|
|
var item Item
|
|
err := json.NewDecoder(r.Body).Decode(&item)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
|
|
return
|
|
}
|
|
|
|
if item.Name == "" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Name is required"})
|
|
return
|
|
}
|
|
|
|
result, err := db.Exec("INSERT INTO items (name) VALUES (?)", item.Name)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to create item"})
|
|
return
|
|
}
|
|
|
|
id, _ := result.LastInsertId()
|
|
item.ID = int(id)
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(item)
|
|
}
|
|
|
|
func handleItemDelete(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.Method != "DELETE" {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Method not allowed"})
|
|
return
|
|
}
|
|
|
|
// Extract ID from URL path /items/{id}
|
|
path := strings.TrimPrefix(r.URL.Path, "/items/")
|
|
id, err := strconv.Atoi(path)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid item ID"})
|
|
return
|
|
}
|
|
|
|
result, err := db.Exec("DELETE FROM items WHERE id = ?", id)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to delete item"})
|
|
return
|
|
}
|
|
|
|
rowsAffected, _ := result.RowsAffected()
|
|
if rowsAffected == 0 {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Item not found"})
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{"message": "Item deleted successfully"})
|
|
}
|
|
|
|
func handleAuthUser(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.Method != "GET" {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Method not allowed"})
|
|
return
|
|
}
|
|
|
|
// Collect all headers for debugging
|
|
headers := make(map[string]string)
|
|
for name, values := range r.Header {
|
|
headers[name] = strings.Join(values, ", ")
|
|
}
|
|
|
|
// Extract common Authelia headers
|
|
userInfo := UserInfo{
|
|
Username: r.Header.Get("Remote-User"),
|
|
Name: r.Header.Get("Remote-Name"),
|
|
Email: r.Header.Get("Remote-Email"),
|
|
Groups: r.Header.Get("Remote-Groups"),
|
|
Headers: headers,
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(userInfo)
|
|
}
|
|
|
|
func handleMining(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.Method != "GET" {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Method not allowed"})
|
|
return
|
|
}
|
|
|
|
// Get the latest record to calculate current value
|
|
var latest MiningRecord
|
|
err := db.QueryRow(`
|
|
SELECT id, timestamp, value, rate
|
|
FROM mining
|
|
ORDER BY timestamp DESC
|
|
LIMIT 1
|
|
`).Scan(&latest.ID, &latest.Timestamp, &latest.Value, &latest.Rate)
|
|
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to get latest mining data"})
|
|
return
|
|
}
|
|
|
|
// Calculate current value based on time elapsed and rate
|
|
now := time.Now()
|
|
minutesElapsed := now.Sub(latest.Timestamp).Minutes()
|
|
currentValue := latest.Value + (latest.Rate * minutesElapsed)
|
|
|
|
// Get recent history (last 5 records)
|
|
rows, err := db.Query(`
|
|
SELECT id, timestamp, value, rate
|
|
FROM mining
|
|
ORDER BY timestamp DESC
|
|
LIMIT 5
|
|
`)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to get mining history"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var history []MiningRecord
|
|
for rows.Next() {
|
|
var record MiningRecord
|
|
err := rows.Scan(&record.ID, &record.Timestamp, &record.Value, &record.Rate)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to scan mining record"})
|
|
return
|
|
}
|
|
history = append(history, record)
|
|
}
|
|
|
|
response := MiningData{
|
|
CurrentValue: currentValue,
|
|
Rate: latest.Rate,
|
|
History: history,
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
func startMiningTicker() {
|
|
ticker := time.NewTicker(1 * time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
log.Println("Mining ticker started - updating every minute")
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
updateMiningValue()
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateMiningValue() {
|
|
// Get the latest record
|
|
var latest MiningRecord
|
|
err := db.QueryRow(`
|
|
SELECT id, timestamp, value, rate
|
|
FROM mining
|
|
ORDER BY timestamp DESC
|
|
LIMIT 1
|
|
`).Scan(&latest.ID, &latest.Timestamp, &latest.Value, &latest.Rate)
|
|
|
|
if err != nil {
|
|
log.Printf("Failed to get latest mining record: %v", err)
|
|
return
|
|
}
|
|
|
|
// Calculate new value based on time elapsed and rate
|
|
now := time.Now()
|
|
minutesElapsed := now.Sub(latest.Timestamp).Minutes()
|
|
newValue := latest.Value + (latest.Rate * minutesElapsed)
|
|
|
|
// Insert new record
|
|
_, err = db.Exec(`
|
|
INSERT INTO mining (timestamp, value, rate)
|
|
VALUES (?, ?, ?)
|
|
`, now, newValue, latest.Rate)
|
|
|
|
if err != nil {
|
|
log.Printf("Failed to insert new mining record: %v", err)
|
|
return
|
|
}
|
|
|
|
log.Printf("Mining updated: %.2f -> %.2f (rate: %.2f/min, elapsed: %.2f min)",
|
|
latest.Value, newValue, latest.Rate, minutesElapsed)
|
|
}
|