diff --git a/cmd/nutmix/main.go b/cmd/nutmix/main.go index 6306a1c..cf9e3c5 100644 --- a/cmd/nutmix/main.go +++ b/cmd/nutmix/main.go @@ -49,6 +49,8 @@ func main() { log.Panicf("os.OpenFile(pathToProjectLogFile, os.O_RDWR|os.O_CREATE, 0764) %+v", err) } + defer logFile.Close() + w := io.MultiWriter(os.Stdout, logFile) opts := &slog.HandlerOptions{ diff --git a/internal/database/admin.go b/internal/database/admin.go index d8c385f..0bf697d 100644 --- a/internal/database/admin.go +++ b/internal/database/admin.go @@ -19,11 +19,13 @@ func GetMintMeltBalanceByTime(pool *pgxpool.Pool, time int64) (MintMeltBalance, var mintMeltBalance MintMeltBalance // change the paid status of the quote batch := pgx.Batch{} - batch.Queue("SELECT quote, request, request_paid, expiry, unit, minted, state, seen_at FROM mint_request WHERE seen_at >= $1", time) - batch.Queue("SELECT quote, request, amount, request_paid, expiry, unit, melted, fee_reserve, state, payment_preimage, seen_at FROM melt_request WHERE seen_at >= $1", time) + batch.Queue("SELECT quote, request, request_paid, expiry, unit, minted, state, seen_at FROM mint_request WHERE seen_at >= $1 AND (state = 'ISSUED' OR state = 'PAID') ", time) + batch.Queue("SELECT quote, request, amount, request_paid, expiry, unit, melted, fee_reserve, state, payment_preimage, seen_at FROM melt_request WHERE seen_at >= $1 AND (state = 'ISSUED' OR state = 'PAID')", time) results := pool.SendBatch(context.Background(), &batch) + defer results.Close() + mintRows, err := results.Query() if err != nil { if err == pgx.ErrNoRows { @@ -47,6 +49,7 @@ func GetMintMeltBalanceByTime(pool *pgxpool.Pool, time int64) (MintMeltBalance, } return mintMeltBalance, databaseError(fmt.Errorf(" results.Query(): %w", err)) } + defer meltRows.Close() meltRequest, err := pgx.CollectRows(meltRows, pgx.RowToStructByName[cashu.MeltRequestDB]) if err != nil { diff --git a/internal/database/main.go b/internal/database/main.go index b63c73f..bcff339 100644 --- a/internal/database/main.go +++ b/internal/database/main.go @@ -339,7 +339,7 @@ func CheckListOfProofs(pool *pgxpool.Pool, CList []string, SecretList []string) var proofList []cashu.Proof - rows, err := pool.Query(context.Background(), "SELECT amount, id, secret, c, y, witness FROM proofs WHERE C = ANY($1) OR secret = ANY($2)", CList, SecretList) + rows, err := pool.Query(context.Background(), "SELECT amount, id, secret, c, y, witness, seen_at FROM proofs WHERE C = ANY($1) OR secret = ANY($2)", CList, SecretList) if err != nil { if err == pgx.ErrNoRows { @@ -394,9 +394,10 @@ func CheckListOfProofsBySecretCurve(pool *pgxpool.Pool, Ys []string) ([]cashu.Pr var proofList []cashu.Proof - rows, err := pool.Query(context.Background(), "SELECT amount, id, secret, c, y, witness FROM proofs WHERE Y = ANY($1)", Ys) + rows, err := pool.Query(context.Background(), `SELECT amount, id, secret, c, y, witness, seen_at FROM proofs WHERE y = ANY($1)`, Ys) if err != nil { + if err == pgx.ErrNoRows { return proofList, nil } diff --git a/internal/routes/admin/auth.go b/internal/routes/admin/auth.go index 30a9f10..eddd773 100644 --- a/internal/routes/admin/auth.go +++ b/internal/routes/admin/auth.go @@ -68,7 +68,9 @@ func AuthMiddleware(ctx context.Context) gin.HandlerFunc { return default: c.Redirect(http.StatusTemporaryRedirect, "/admin/login") - c.Next() + c.Header("HX-Location", "/admin/login") + c.Abort() + c.JSON(200, nil) } } diff --git a/internal/routes/admin/keysets.go b/internal/routes/admin/keysets.go index 557e86b..2bbc73f 100644 --- a/internal/routes/admin/keysets.go +++ b/internal/routes/admin/keysets.go @@ -16,7 +16,7 @@ func KeysetsPage(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.H return func(c *gin.Context) { - c.HTML(200, "keysets-page", nil) + c.HTML(200, "keysets.html", nil) } } func KeysetsLayoutPage(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { diff --git a/internal/routes/admin/main.go b/internal/routes/admin/main.go index 5339e64..16bcf46 100644 --- a/internal/routes/admin/main.go +++ b/internal/routes/admin/main.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "slices" + "time" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgxpool" @@ -35,34 +36,92 @@ func AdminRoutes(ctx context.Context, r *gin.Engine, pool *pgxpool.Pool, mint *m adminRoute.Use(AuthMiddleware(ctx)) + // PAGES SETUP + // This is /admin adminRoute.GET("", InitPage(ctx, pool, mint)) + adminRoute.GET("/keysets", KeysetsPage(ctx, pool, mint)) + adminRoute.GET("/settings", MintSettingsPage(ctx, pool, mint)) adminRoute.GET("/login", LoginPage(ctx, pool, mint)) - adminRoute.POST("/login", Login(ctx, pool, mint)) + adminRoute.GET("/bolt11", LightningNodePage(ctx, pool, mint)) - // partial template routes - adminRoute.GET("/mintsettings", MintInfoTab(ctx, pool, mint)) - adminRoute.POST("/mintsettings", MintInfoPost(ctx, pool, mint)) - - adminRoute.GET("/bolt11", Bolt11Tab(ctx, pool, mint)) + // change routes + adminRoute.POST("/login", Login(ctx, pool, mint)) + adminRoute.POST("/mintsettings", MintSettingsForm(ctx, pool, mint)) adminRoute.POST("/bolt11", Bolt11Post(ctx, pool, mint)) - - adminRoute.GET("/keysets", KeysetsPage(ctx, pool, mint)) - adminRoute.POST("/rotate/sats", RotateSatsSeed(ctx, pool, mint)) - adminRoute.GET("/keysets-layout", KeysetsLayoutPage(ctx, pool, mint)) + // fractional html components + adminRoute.GET("/keysets-layout", KeysetsLayoutPage(ctx, pool, mint)) adminRoute.GET("/lightningdata", LightningDataFormFields(ctx, pool, mint)) - - adminRoute.GET("/mintactivity", MintActivityTab(ctx, pool, mint)) adminRoute.GET("/mint-balance", MintBalance(ctx, pool, mint)) - adminRoute.GET("/mint-melt", MintMeltActivity(ctx, pool, mint)) - + adminRoute.GET("/mint-melt-summary", MintMeltSummary(ctx, pool, mint)) + adminRoute.GET("/mint-melt-list", MintMeltList(ctx, pool, mint)) adminRoute.GET("/logs", LogsTab(ctx)) } + +type TIME_REQUEST string + +var ( + h24 TIME_REQUEST = "24h" + h48 TIME_REQUEST = "48h" + h72 TIME_REQUEST = "72h" + d7 TIME_REQUEST = "7D" + ALL TIME_REQUEST = "all" +) + +func ParseToTimeRequest(str string) TIME_REQUEST { + + switch str { + case "24h": + return h24 + case "48h": + return h48 + case "72h": + return h72 + case "7d": + return d7 + case "all": + return ALL + default: + return h24 + } + +} + +// return 24 hours by default +func (t TIME_REQUEST) RollBackFromNow() time.Time { + + rollBackHour := time.Now() + + switch t { + case h24: + duration := time.Duration(24) * time.Hour + return rollBackHour.Add(-duration) + case h48: + duration := time.Duration(48) * time.Hour + return rollBackHour.Add(-duration) + case h72: + duration := time.Duration(72) * time.Hour + return rollBackHour.Add(-duration) + case d7: + duration := time.Duration((7 * 24)) * time.Hour + return rollBackHour.Add(-duration) + case ALL: + return time.Unix(1, 0) + } + duration := time.Duration(24) * time.Hour + return rollBackHour.Add(-duration) +} + func LogsTab(ctx context.Context) gin.HandlerFunc { return func(c *gin.Context) { + + timeHeader := c.GetHeader("time") + + timeRequestDuration := ParseToTimeRequest(timeHeader) + // read logs logsdir, err := utils.GetLogsDirectory() @@ -71,6 +130,7 @@ func LogsTab(ctx context.Context) gin.HandlerFunc { } file, err := os.Open(logsdir + "/" + mint.LogFileName) + defer file.Close() if err != nil { errorMessage := ErrorNotif{ @@ -81,7 +141,7 @@ func LogsTab(ctx context.Context) gin.HandlerFunc { return } - logs := utils.ParseLogFileByLevel(file, []slog.Level{slog.LevelWarn, slog.LevelError, slog.LevelInfo}) + logs := utils.ParseLogFileByLevelAndTime(file, []slog.Level{slog.LevelWarn, slog.LevelError, slog.LevelInfo, slog.LevelDebug}, timeRequestDuration.RollBackFromNow()) slices.Reverse(logs) diff --git a/internal/routes/admin/mint-activity.go b/internal/routes/admin/mint-activity.go index 475da5c..6bc719c 100644 --- a/internal/routes/admin/mint-activity.go +++ b/internal/routes/admin/mint-activity.go @@ -4,8 +4,8 @@ import ( "context" "fmt" "log" + "sort" "time" - "github.com/btcsuite/btcd/btcutil" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgxpool" @@ -15,16 +15,10 @@ import ( "github.com/lightningnetwork/lnd/zpay32" ) -func MintActivityTab(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { - - return func(c *gin.Context) { - c.HTML(200, "mint-activity", nil) - } -} - func MintBalance(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { return func(c *gin.Context) { + if mint.Config.MINT_LIGHTNING_BACKEND == comms.FAKE_WALLET { c.HTML(200, "fake-wallet-balance", nil) return @@ -46,13 +40,14 @@ func MintBalance(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.H } } -func MintMeltActivity(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { +func MintMeltSummary(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { return func(c *gin.Context) { - duration := time.Duration(24) * time.Hour - previous24hours := time.Now().Add(-duration).Unix() + timeHeader := c.GetHeader("time") + + timeRequestDuration := ParseToTimeRequest(timeHeader) - mintMeltBalance, err := database.GetMintMeltBalanceByTime(pool, previous24hours) + mintMeltBalance, err := database.GetMintMeltBalanceByTime(pool, timeRequestDuration.RollBackFromNow().Unix()) if err != nil { log.Println(err) @@ -65,8 +60,8 @@ func MintMeltActivity(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) return } - mintMeltTotal := make(map[string]float64) - + mintMeltTotal := make(map[string]int64) + mintMeltTotal["Mint"] += 0 // sum up mint for _, mintRequest := range mintMeltBalance.Mint { invoice, err := zpay32.Decode(mintRequest.Request, &mint.Network) @@ -82,19 +77,93 @@ func MintMeltActivity(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) return } - mintMeltTotal["Mint"] += invoice.MilliSat.ToSatoshis().ToUnit(btcutil.AmountSatoshi) + mintMeltTotal["Mint"] += int64( invoice.MilliSat.ToSatoshis().ToUnit(btcutil.AmountSatoshi)) } // sum up melt amount for _, meltRequest := range mintMeltBalance.Melt { - mintMeltTotal["Melt"] += float64(meltRequest.Amount) + mintMeltTotal["Melt"] += int64(meltRequest.Amount) } mintMeltTotal["Melt"] = mintMeltTotal["Melt"] * -1 // get net flows - mintMeltTotal["Net"] = mintMeltTotal["Mint"] - mintMeltTotal["Melt"] + mintMeltTotal["Net"] = mintMeltTotal["Mint"] + mintMeltTotal["Melt"] c.HTML(200, "mint-melt-activity", mintMeltTotal) } } +func MintMeltList(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { + return func(c *gin.Context) { + timeHeader := c.GetHeader("time") + timeRequestDuration := ParseToTimeRequest(timeHeader) + + mintMeltBalance, err := database.GetMintMeltBalanceByTime(pool, timeRequestDuration.RollBackFromNow().Unix()) + + if err != nil { + log.Println(err) + errorMessage := ErrorNotif{ + + Error: "There was an error getting mint activity", + } + + c.HTML(200, "settings-error", errorMessage) + return + } + + mintMeltRequestVisual := ListMintMeltVisual{} + + // sum up mint + for _, mintRequest := range mintMeltBalance.Mint { + utc := time.Unix(mintRequest.SeenAt, 0).UTC().Format("2006-Jan-2 15:04:05 MST") + + mintMeltRequestVisual = append(mintMeltRequestVisual, MintMeltRequestVisual{ + Type: "Mint", + Unit: mintRequest.Unit, + Request: mintRequest.Request, + Status: string(mintRequest.State), + SeenAt: utc, + }) + + } + + // sum up melt amount + for _, meltRequest := range mintMeltBalance.Melt { + utc := time.Unix(meltRequest.SeenAt, 0).UTC().Format("2006-Jan-2 15:04:05 MST") + + mintMeltRequestVisual = append(mintMeltRequestVisual, MintMeltRequestVisual{ + Type: "Melt", + Unit: meltRequest.Unit, + Request: meltRequest.Request, + Status: string(meltRequest.State), + SeenAt: utc, + }) + } + + sort.Sort(mintMeltRequestVisual) + + c.HTML(200, "mint-melt-list", mintMeltRequestVisual) + } +} + +type MintMeltRequestVisual struct { + Type string + Unit string + Request string + Status string + SeenAt string +} + +type ListMintMeltVisual []MintMeltRequestVisual + +func (ms ListMintMeltVisual) Len() int { + return len(ms) +} + +func (ms ListMintMeltVisual) Less(i, j int) bool { + return ms[i].SeenAt < ms[j].SeenAt +} + +func (ms ListMintMeltVisual) Swap(i, j int) { + ms[i], ms[j] = ms[j], ms[i] +} diff --git a/internal/routes/admin/pages.go b/internal/routes/admin/pages.go index 1d67b1e..35d645f 100644 --- a/internal/routes/admin/pages.go +++ b/internal/routes/admin/pages.go @@ -44,6 +44,6 @@ func LoginPage(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.Han func InitPage(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { return func(c *gin.Context) { - c.HTML(200, "index.html", nil) + c.HTML(200, "mint_activity.html", nil) } } diff --git a/internal/routes/admin/static/activity.css b/internal/routes/admin/static/activity.css new file mode 100644 index 0000000..09f894e --- /dev/null +++ b/internal/routes/admin/static/activity.css @@ -0,0 +1,100 @@ +.activity { + display: flex; + justify-content: center; + align-content: center; + padding: 0% 10% +} +.balance { + display: flex; + gap: 35px; + border-radius: 15px; +} + +.data-list { + display: flex; + flex-direction: row; + margin-top: 25px; + gap: 15px; + /* width: 100; */ +} + +.time-select { + margin-bottom: 15px; +} + +button.time-button { + border-radius: 0px; + background-color: transparent; + color: var(--white); + border: 1px solid var(--white); + margin-left: 10px; + margin-right: 10px; +} + +button.time-button.selected { + background-color: transparent; + color: var(--darkreader-inline-color); +} + +.data-table { + width: 50%; +} +.data-table h2 { + padding-left: 10px; +} + +.data-table div { + display: flex; + flex-direction: column; + overflow-y: scroll; + overflow-x: hidden; + overflow-wrap: break-word; + max-height: 500px; +} + +.data-table caption { + text-align: left; + padding-left: 15px; +} + + +tr.DEBUG { + background-color: #94ed74; + color: black; +} + +tr.WARN { + background-color: #f1fc74; + color: black; +} + +tr.ERROR { + background-color: #ed6565; + color: black; +} + +.request-row { + max-width: 300px; + text-overflow: ellipsis; +} + +th, +td { + border: 1px solid rgb(160 160 160); + padding: 8px 10px; + text-align: center; +} + +td.unit { + text-transform: uppercase; +} + +tr.Mint { + background-color: #94ed74; + color: black; +} + +tr.Melt { + background-color: #ed6565; + color: black; +} diff --git a/internal/routes/admin/static/app.css b/internal/routes/admin/static/app.css index 1bb2c5f..2f093a8 100644 --- a/internal/routes/admin/static/app.css +++ b/internal/routes/admin/static/app.css @@ -1,208 +1,270 @@ +@import url("settings.css"); +@import url("bolt11.css"); +@import url("keysets.css"); +@import url("activity.css"); + :root { -/** CSS DARK THEME PRIMARY COLORS */ ---color-primary-100: #2196f3; ---color-primary-200: #50a1f5; ---color-primary-300: #6eacf6; ---color-primary-400: #87b8f8; ---color-primary-500: #9dc3f9; ---color-primary-600: #b2cffb; -/** CSS DARK THEME SURFACE COLORS */ ---color-surface-100: #000000; ---color-surface-200: #1e1e1e; ---color-surface-300: #353535; ---color-surface-400: #4e4e4e; ---color-surface-500: #696969; ---color-surface-600: #858585; -/** CSS DARK THEME MIXED SURFACE COLORS */ ---color-surface-mixed-100: #0f141b; ---color-surface-mixed-200: #252930; ---color-surface-mixed-300: #3c4046; ---color-surface-mixed-400: #55585e; ---color-surface-mixed-500: #6f7277; ---color-surface-mixed-600: #8a8c90; - ---white: #FFFFFF; -} - -html, body { - min-height: 100%; - height: 100%; - background-color: var(--color-surface-100); - color: var(--white); + /** CSS DARK THEME PRIMARY COLORS */ + --color-primary-100: #382bf0; + --color-primary-200: #5e43f3; + --color-primary-300: #7a5af5; + --color-primary-400: #9171f8; + --color-primary-500: #a688fa; + --color-primary-600: #ba9ffb; + + /** CSS DARK THEME SURFACE COLORS */ + --color-surface-100: #121212; + --color-surface-200: #282828; + --color-surface-300: #3f3f3f; + --color-surface-400: #575757; + --color-surface-500: #717171; + --color-surface-600: #8b8b8b; + + /** CSS DARK THEME MIXED SURFACE COLORS */ + --color-surface-mixed-100: #1a1625; + --color-surface-mixed-200: #2f2b3a; + --color-surface-mixed-300: #46424f; + --color-surface-mixed-400: #5e5a66; + --color-surface-mixed-500: #76737e; + --color-surface-mixed-600: #908d96; + + --white: rgb(232, 230, 227); + + --darkreader-inline-bgcolor: #230578; + --darkreader-inline-color: #ac8bfa; } + + + +* { + box-sizing: border-box; +} + +html, body { - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: center; - margin: 0px; + display: flex; + background-color: var(--color-surface-100); + color: var(--white); +} +body { + display: flex; + flex-direction: column; + width: 100vw; + height: 100vh; + margin: 0; } -form { - display: flex; - flex-direction: column; - background-color: var(--color-surface-200); - gap: 15px; - padding: 0px 30px 30px 30px; - border-radius: 15px; - border: 2px solid #020000; +header { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + padding: 15px 10%; + align-self: flex-start; } -button { - background-color: var(--color-primary-500); - padding: 15px; - border-radius: 50px; - font-weight: 800; - letter-spacing: 1px; - cursor: pointer; +nav { + display: flex; + flex-direction: row; + gap: 10px; } -button.square { - border-radius: 0px; + +nav a { + align-self: center; + font-weight: 600; + font-size: 20px; } -button.selected { - background-color: var(--color-primary-200); - cursor: default; +a { + color: inherit; + text-decoration: none; +} + +a:hover { + color: var(--color-primary-400); +} + +a.selected { + color: var(--darkreader-inline-color); + font-weight: 700; +} + +/* a:vi */ + +main { + width: 100%; + padding: 15px; + display: flex; + flex-direction: column; } -input { - padding: 10px; - letter-spacing: 1px; +form { + display: flex; + flex-direction: column; + background-color: var(--color-surface-200); + gap: 15px; + padding: 0px 30px 30px 30px; + border-radius: 15px; + border: 2px solid #020000; +} + +button { + background-color: var(--darkreader-inline-bgcolor); + color: var(--white); + padding: 15px 20px; + border-radius: 50px; + + font-weight: 800; + font-size: 14px; + letter-spacing: 1px; + border: 0px; +} +button:hover { + cursor: pointer; + box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px; } -.content { - min-width: 90%; +button.selected { + background-color: var(--color-primary-200); + cursor: default; } -.tab-content { - margin-top: 36px; +input, +textarea, +select, +option { + padding: 10px; + letter-spacing: 1px; + background: transparent; + color: var(--white); } -.tab-list { - margin-top: 25px; +option { + background-color: var(--color-surface-200); } .settings-input { - display: flex; - flex-direction: column; - padding: 20px; - background-color: var(--color-surface-300); - border-radius: 25px; - gap: 10px; + display: flex; + flex-direction: column; + padding: 5px; + gap: 10px; + width: 100%; +} +.settings-input input { + border: solid 1px var(--white); + background-color: transparent; +} +.settings-input input:focus { + outline: var(--color-primary-100) solid 1px; + border: solid 1px var(--color-primary-100); } .settings-input textarea { - min-height: 120px; + background-color: transparent; + min-height: 120px; + border: solid 1px var(--white); +} +.settings-input textarea:focus { + outline: var(--color-primary-100) solid 1px; + border: solid 1px var(--color-primary-100); } -select { - padding: 20px; - border-radius: 25px; - gap: 10px; +.settings-input-checkbox { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 20px; + background-color: transparent; + border-radius: 25px; + gap: 10px; + width: 100%; +} +select, +option { + /* padding: 20px; */ + /* border-radius: 25px; */ + gap: 10px; } .form-section { - display: flex; - flex-direction: row; - gap: 15px; - + display: flex; + flex-direction: row; + gap: 15px; + width: 100%; } #notifications { - display: flex; - justify-content: center; - color: black; + display: flex; + justify-content: center; + color: black; } #notifications > .success { - background-color: #90ef6e; - margin-top: 15px; - margin-bottom: -15px; - padding: 10px 25px; - border-radius: 25px; + background-color: #90ef6e; + margin-top: 15px; + margin-bottom: -15px; + padding: 10px 25px; + border-radius: 25px; } #notifications > .error { - background-color: #ff6666; - margin-top: 15px; - margin-bottom: -15px; - padding: 10px 25px; - border-radius: 25px; + background-color: #ff6666; + margin-top: 15px; + margin-bottom: -15px; + padding: 10px 25px; + border-radius: 25px; +} + +.login { + display: flex; + height: 100%; + justify-content: center; + align-items: center; } .login-form { - align-content: center; - justify-content: center; + align-content: center; + justify-content: center; } #nip07-form { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; + max-width: 560px; } #lightning-data { - display: flex; - flex-direction: column; - gap: 15px; + display: flex; + flex-direction: column; + gap: 15px; } -.fees-form { - flex-direction: row; - padding: 15px; - margin: 0px; - align-items: center; - /* margin: 15px; */ - +.md-button { + max-width: 360px; } -.keysets { - display: flex; - flex-wrap: wrap; - gap: 15px; - margin-top: 30px; -} - -.balance { - display: flex; - gap: 35px; -} .card { - display: flex; - flex-direction: column; - gap: 5px; - border: 1px solid var(--color-primary-500); - width: fit-content; - padding: 10px 15px; + display: flex; + flex-direction: column; + gap: 5px; + background-color: var(--color-surface-200); + border: 1px solid var(--white); + min-width: 200px; + padding: 15px 15px; + border-radius: 15px; } - - - - - - - - - - - - - - - - - - /* lds loading ring */ .lds-dual-ring { /* change color here */ color: #1c4c5b; - margin-bottom:15px; + margin-bottom: 15px; } .lds-dual-ring, .lds-dual-ring:after { @@ -232,25 +294,3 @@ select { transform: rotate(360deg); } } - - - - - - - - - - - - - - - - - - - - - - diff --git a/internal/routes/admin/static/app.js b/internal/routes/admin/static/app.js index 963a4b2..b9afdca 100644 --- a/internal/routes/admin/static/app.js +++ b/internal/routes/admin/static/app.js @@ -37,7 +37,7 @@ nip07form?.addEventListener("submit", (e) => { .signEvent(eventToSign) .then( ( - /** + /** @type {SignedNostrEvent} */ signedEvent ) => { @@ -77,3 +77,29 @@ nip07form?.addEventListener("submit", (e) => { console.log({ err }); }); }); + +// check for click on button for age of logs + +/** + * @type NodeListOf + * */ +const buttons = document.querySelectorAll(".time-button"); + +document.querySelector(".time-select")?.addEventListener("click", (evt) => { + // turn all time buttons off by removing class + if (buttons) { + for (let i = 0; i < buttons.length; i++) { + const element = buttons[i]; + + element.classList.remove("selected"); + } + } + + evt.target?.classList.add("selected"); + window.htmx.trigger(".summary-table", "reload", { time: evt.target?.value }); + window.htmx.trigger(".log-table", "reload", { time: evt.target?.value }); + window.htmx.trigger(".mint-melt-table ", "reload", { + time: evt.target?.value, + }); +}); +// diff --git a/internal/routes/admin/static/bolt11.css b/internal/routes/admin/static/bolt11.css new file mode 100644 index 0000000..cf93953 --- /dev/null +++ b/internal/routes/admin/static/bolt11.css @@ -0,0 +1,7 @@ +.bolt11 { + width: 50%; + align-self: center; +} +.bolt11 h2 { + text-align: center; +} diff --git a/internal/routes/admin/static/keysets.css b/internal/routes/admin/static/keysets.css new file mode 100644 index 0000000..27858a9 --- /dev/null +++ b/internal/routes/admin/static/keysets.css @@ -0,0 +1,32 @@ +.keysets { + + display: flex; + justify-content: center; + align-content: center; + padding: 0% 10% + +} +.fees-form { + flex-direction: row; + padding: 15px; + margin: 0px; + align-items: center; + width: fit-content; + border: 1px solid var(--white); +} + +.keysets-list { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-self: center; + gap: 15px; + margin-top: 30px; + border-radius: 15px; +} + +.unit { + text-transform: uppercase; + +} + diff --git a/internal/routes/admin/static/settings.css b/internal/routes/admin/static/settings.css new file mode 100644 index 0000000..cafd424 --- /dev/null +++ b/internal/routes/admin/static/settings.css @@ -0,0 +1,7 @@ +.settings { + width: 50%; + align-self: center; +} +.settings h2 { + text-align: center; +} diff --git a/internal/routes/admin/tabs.go b/internal/routes/admin/tabs.go index da5e2f0..8b57f64 100644 --- a/internal/routes/admin/tabs.go +++ b/internal/routes/admin/tabs.go @@ -2,23 +2,24 @@ package admin import ( "context" + "log" + "strconv" + "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/internal/comms" "github.com/lescuer97/nutmix/internal/mint" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" - "log" - "strconv" ) -func MintInfoTab(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { - +func MintSettingsPage(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { return func(c *gin.Context) { - c.HTML(200, "mint-settings", mint.Config) + c.HTML(200, "settings.html", mint.Config) } } -func MintInfoPost(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { + +func MintSettingsForm(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { return func(c *gin.Context) { // check the different variables that could change @@ -135,12 +136,13 @@ func MintInfoPost(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin. c.HTML(200, "settings-success", successMessage) } } -func Bolt11Tab(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { +func LightningNodePage(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { return func(c *gin.Context) { - c.HTML(200, "bolt11-info", mint.Config) + c.HTML(200, "bolt11.html", mint.Config) } } + func Bolt11Post(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { return func(c *gin.Context) { @@ -152,16 +154,16 @@ func Bolt11Post(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.Ha } // check if the the lightning values have change if yes try to setup a new connection client for mint - mint.Config.NAME = c.Request.PostFormValue("NETWORK") - + if mint.Config.NETWORK != c.Request.PostFormValue("NETWORK") { + mint.Config.NETWORK = c.Request.PostFormValue("NETWORK") + successMessage.Success = "Network changed" + } switch c.Request.PostFormValue("MINT_LIGHTNING_BACKEND") { case comms.FAKE_WALLET: mint.Config.MINT_LIGHTNING_BACKEND = comms.FAKE_WALLET - successMessage.Success = "Nothing to change" - c.HTML(200, "settings-success", successMessage) case comms.LND_WALLET: lndHost := c.Request.PostFormValue("LND_GRPC_HOST") @@ -205,12 +207,10 @@ func Bolt11Post(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.Ha mint.Config.LND_GRPC_HOST = newCommsData.LND_GRPC_HOST mint.Config.LND_MACAROON = newCommsData.LND_MACAROON mint.Config.LND_TLS_CERT = newCommsData.LND_TLS_CERT - c.HTML(200, "settings-success", successMessage) } else { + mint.Config.MINT_LIGHTNING_BACKEND = comms.LND_WALLET successMessage.Success = "Nothing to change" - c.HTML(200, "settings-success", successMessage) - } case comms.LNBITS_WALLET: @@ -252,7 +252,6 @@ func Bolt11Post(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.Ha mint.Config.MINT_LIGHTNING_BACKEND = newCommsData.MINT_LIGHTNING_BACKEND mint.Config.MINT_LNBITS_KEY = newCommsData.MINT_LNBITS_KEY mint.Config.MINT_LNBITS_ENDPOINT = newCommsData.MINT_LNBITS_ENDPOINT - c.HTML(200, "settings-success", successMessage) } @@ -260,7 +259,7 @@ func Bolt11Post(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.Ha if err != nil { log.Println("mint.Config.SetTOMLFile() %w", err) errorMessage := ErrorNotif{ - Error: "there was a problem in the server", + Error: "There was a problem setting your config", } c.HTML(200, "settings-error", errorMessage) @@ -268,6 +267,7 @@ func Bolt11Post(ctx context.Context, pool *pgxpool.Pool, mint *mint.Mint) gin.Ha } + c.HTML(200, "settings-success", successMessage) return } } diff --git a/internal/routes/admin/templates/base.html b/internal/routes/admin/templates/base.html index 4fbedc3..52ce171 100644 --- a/internal/routes/admin/templates/base.html +++ b/internal/routes/admin/templates/base.html @@ -1,15 +1,16 @@ {{ define "base" }} - - - + + + + - - + + {{ end }} - - - - diff --git a/internal/routes/admin/templates/bolt11.html b/internal/routes/admin/templates/bolt11.html new file mode 100644 index 0000000..f1a91fd --- /dev/null +++ b/internal/routes/admin/templates/bolt11.html @@ -0,0 +1,76 @@ +{{ template "base" }} + +{{template "navigation" "bolt11"}} + +
+ +
+
+
+

Lightning Node info

+ + + + + + +
+ + +
+
+ +{{ define "lnd-grpc-form" }} + + + + + +{{ end }} + +{{ define "fake-wallet-form" }} + + It's a fake lightning wallet so you don't have to set anything up + +{{ end }} + +{{ define "lnbits-wallet-form" }} + + + + +{{ end }} +{{ define "problem-form" }} + "something is wrong you sent the wrong lightning backend" +{{ end }} diff --git a/internal/routes/admin/templates/error.html b/internal/routes/admin/templates/error.html index 6fca1a0..89f1df1 100644 --- a/internal/routes/admin/templates/error.html +++ b/internal/routes/admin/templates/error.html @@ -1,6 +1,3 @@ - {{ template "base" }} -
- Something went wrong... -
+
Something went wrong...
diff --git a/internal/routes/admin/templates/general.html b/internal/routes/admin/templates/general.html new file mode 100644 index 0000000..e0aabad --- /dev/null +++ b/internal/routes/admin/templates/general.html @@ -0,0 +1,30 @@ +{{define "navigation"}} +
+

Nutmix Dashboard

+ + + +
+ +{{end}} + +{{ define "settings-error" }} + +
+ {{.Error}} +
+ +{{end}} + +{{ define "settings-success" }} + +
+ {{.Success}} +
+ +{{end}} diff --git a/internal/routes/admin/templates/index.html b/internal/routes/admin/templates/index.html deleted file mode 100644 index 27ab189..0000000 --- a/internal/routes/admin/templates/index.html +++ /dev/null @@ -1,14 +0,0 @@ -{{ template "base" }} - -
-
-
- - -{{ define "header" }} - - ok - -{{ end }} - - diff --git a/internal/routes/admin/templates/keysets.html b/internal/routes/admin/templates/keysets.html new file mode 100644 index 0000000..2b082fc --- /dev/null +++ b/internal/routes/admin/templates/keysets.html @@ -0,0 +1,49 @@ +{{ template "base" }} {{template "navigation" "keysets"}} + +
+
+ +
+ + + + +
+
+ +
+
+ +{{ define "keysets" }} {{range .Keysets}} {{template "keyset-card" .}} {{end}} +{{ end }} + + +{{ define "keyset-card" }} + +
+ Id: {{.Id}} + Active: {{.Active}} + Unit: {{.Unit}} + Fees (PPK): {{.Fees}} + Version: {{.Version}} +
+ +{{end}} diff --git a/internal/routes/admin/templates/login.html b/internal/routes/admin/templates/login.html index daf1ee7..c4b33b9 100644 --- a/internal/routes/admin/templates/login.html +++ b/internal/routes/admin/templates/login.html @@ -1,32 +1,31 @@ {{ template "base" }} - -{{ if eq .ADMINNPUB "" }} - -
- -

Please set the "ADMIN_NOSTR_NPUB" in the .env file to be able to access the Admin dashboard

- -
- -{{ else }} -
+
+ {{ if eq .ADMINNPUB "" }} + +
+

+ Please set the "ADMIN_NOSTR_NPUB" in the .env file to be able + to access the Admin dashboard +

+
+ + {{ else }} + - -{{ end }} +
+
+ {{ end }} + {{ define "incorrect-key-error" }} -
- The correct private key was not used to sign in. -
+
The correct private key was not used to sign in.
{{ end }} diff --git a/internal/routes/admin/templates/mint_activity.html b/internal/routes/admin/templates/mint_activity.html index 9dbe50a..3038cd6 100644 --- a/internal/routes/admin/templates/mint_activity.html +++ b/internal/routes/admin/templates/mint_activity.html @@ -1,81 +1,130 @@ +{{ template "base" }} -{{ define "mint-activity" }} - +{{template "navigation" "activity"}} -
- - -
+
+
+
+ + + + +
+
+
-
- - -

Last 24 hours activity

- -
- -
-
+
+
+
+
+
+ +
+
-
-
+{{ define "node-balance"}} +
+

Node Balance:

+ {{.}} Sats +
+{{ end }} -
+

Fake Wallet doesn't have a balance

+
+{{ end }} - style=" - overflow: scroll; - max-height: 500px; - display: flex; - flex-direction: column; - "> - -
- +{{ define "mint-melt-activity"}} +
+
Inflows: {{printf "%d" .Mint }} Sats
-
+
Outflows: {{.Melt}} Sats
- - +
Net flows: {{.Net}} Sats
+ +{{ end }} +{{ define "logs" }} +

Logs

+
+ + + + + + + + + + {{range .}} + + + + + + {{end}} + +
LevelMessageTime
{{.Level}}{{.Msg}}{{.Time}}
+
-
{{ end }} -{{ define "node-balance"}} -
-

Node Balance:

- {{.}} Sats -
-{{ end }} +{{ define "mint-melt-list" }} -{{ define "fake-wallet-balance"}} -
-

Fake Wallet doesn't have a balance

-
-{{ end }} +

Mint & Melt

+
+ + + + + + + + + + + + {{range .}} + + + + + + + + {{end}} + +
TypeRequestUnitStatusTime
{{.Type}}{{.Request}}{{.Unit}}{{.Status}}{{.SeenAt}}
+
-{{ define "mint-melt-activity"}} -
-
- Mint: {{.Mint}} Sats -
- -
- Melt: {{.Melt}} Sats -
- -
- Net inflows: {{.Net}} Sats -
-
{{ end }} diff --git a/internal/routes/admin/templates/settings.html b/internal/routes/admin/templates/settings.html new file mode 100644 index 0000000..00144de --- /dev/null +++ b/internal/routes/admin/templates/settings.html @@ -0,0 +1,83 @@ +{{ template "base" }} {{template "navigation" "settings"}} + +
+
+
+

Mint Settings

+ +
+ + + + +
+
+ +
+ +
+ + +
+ +

Lightning Options

+
+ + + +
+ + +
+
diff --git a/internal/routes/admin/templates/tabs.html b/internal/routes/admin/templates/tabs.html deleted file mode 100644 index ed3c798..0000000 --- a/internal/routes/admin/templates/tabs.html +++ /dev/null @@ -1,270 +0,0 @@ -{{ define "mint-settings" }} - - -
- - -
-
-
-

Mint Settings

- - - - - - -
- - - - - - - -
- - -
-
- -{{ end }} - - -{{ define "bolt11-info" }} - - -
-
-
- -
-

Lightning Node info

- - - - - - -
- - -
-
-{{ end }} - - -{{ define "lnd-grpc-form" }} - - - - - -{{ end }} - -{{ define "fake-wallet-form" }} - - It's a fake lightning wallet so you don't have to set anything up - -{{ end }} - -{{ define "lnbits-wallet-form" }} - - - - -{{ end }} - - -{{ define "keysets-page" }} - - -
- - -
-
- -
- - - - - -
-
- - - - -
- -
- - - - -
-{{ end }} - -{{ define "logs" }} - {{range .}} - -

- Level: {{.Level}} - - Message: {{.Msg}} - - Time: {{.Time}} - -

- - - {{end}} - -{{ end }} - - - -{{ define "keysets" }} - {{range .Keysets}} - - {{template "keyset-card" .}} - - - {{end}} - -{{ end }} - -{{ define "keyset-card" }} - -
- Id: {{.Id}} - Active: {{.Active}} - Unit: {{.Unit}} - Fees (PPK): {{.Fees}} - Version: {{.Version}} - -
- -{{end}} - - - -{{ define "problem-form" }} - "something is wrong you sent the wrong lightning backend" -{{ end }} - - - -{{ define "settings-error" }} - -
- {{.Error}} -
- -{{end}} - -{{ define "settings-success" }} - -
- {{.Success}} -
- -{{end}} diff --git a/internal/routes/bolt11.go b/internal/routes/bolt11.go index ce3d01f..3db7dcd 100644 --- a/internal/routes/bolt11.go +++ b/internal/routes/bolt11.go @@ -67,6 +67,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * expireTime := cashu.ExpiryTimeMinUnit(15) now := time.Now().Unix() + logger.Debug(fmt.Sprintf("Requesting invoice for amount: %v. backend: %v", mintRequest.Amount, mint.Config.MINT_LIGHTNING_BACKEND)) switch mint.Config.MINT_LIGHTNING_BACKEND { case comms.FAKE_WALLET: payReq, err := lightning.CreateMockInvoice(mintRequest.Amount, "mock invoice", mint.Network, expireTime) @@ -372,6 +373,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * dbRequest := cashu.MeltRequestDB{} expireTime := cashu.ExpiryTimeMinUnit(15) + now := time.Now().Unix() switch mint.Config.MINT_LIGHTNING_BACKEND { case comms.FAKE_WALLET: @@ -395,8 +397,6 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * PaymentPreimage: "", } - now := time.Now().Unix() - dbRequest = cashu.MeltRequestDB{ Quote: response.Quote, Request: meltRequest.Request, @@ -441,6 +441,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * RequestPaid: response.Paid, State: response.State, PaymentPreimage: response.PaymentPreimage, + SeenAt: now, } default: diff --git a/internal/routes/mint.go b/internal/routes/mint.go index d97dc07..fdff35e 100644 --- a/internal/routes/mint.go +++ b/internal/routes/mint.go @@ -305,15 +305,15 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger *sl return } - mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) - err = database.SetRestoreSigs(pool, recoverySigsDb) if err != nil { + mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) log.Println(fmt.Errorf("SetRecoverySigs: %w", err)) log.Println(fmt.Errorf("recoverySigsDb: %+v", recoverySigsDb)) c.JSON(200, response) return } + mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) c.JSON(200, response) }) @@ -330,7 +330,6 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger *sl checkStateResponse := cashu.PostCheckStateResponse{ States: make([]cashu.CheckState, 0), } - // set as unspent proofs, err := database.CheckListOfProofsBySecretCurve(pool, checkStateRequest.Ys) diff --git a/internal/utils/files.go b/internal/utils/files.go index b96dae5..7a58441 100644 --- a/internal/utils/files.go +++ b/internal/utils/files.go @@ -26,10 +26,11 @@ type SlogRecordJSON struct { Level slog.Level } -func ParseLogFileByLevel(file *os.File, wantedLevel []slog.Level) []SlogRecordJSON { +func ParseLogFileByLevelAndTime(file *os.File, wantedLevel []slog.Level, limitTime time.Time) []SlogRecordJSON { var logRecords []SlogRecordJSON scanner := bufio.NewScanner(file) + // optionally, resize scanner's capacity for lines over 64K, see next example for scanner.Scan() { @@ -40,10 +41,8 @@ func ParseLogFileByLevel(file *os.File, wantedLevel []slog.Level) []SlogRecordJS continue } - if slices.Contains(wantedLevel, logRecord.Level) { - + if slices.Contains(wantedLevel, logRecord.Level) && logRecord.Time.Unix() > limitTime.Unix() { logRecords = append(logRecords, logRecord) - } }