diff --git a/.gqlgen.yml b/.gqlgen.yml
index 1f2b337..80a8256 100644
--- a/.gqlgen.yml
+++ b/.gqlgen.yml
@@ -21,6 +21,9 @@ models:
     model: github.com/99designs/gqlgen/graphql.Time
   Int:
     model: github.com/99designs/gqlgen/graphql.Int64
+  UInt:
+    model:
+      - github.com/99designs/gqlgen/graphql.Uint
   Torrent:
     extraFields:
       T:
diff --git a/graphql/schema.graphql b/graphql/schema.graphql
index e18fd22..fbc3282 100644
--- a/graphql/schema.graphql
+++ b/graphql/schema.graphql
@@ -5,6 +5,7 @@ directive @stream on FIELD_DEFINITION
 
 scalar DateTime
 scalar Upload
+scalar UInt
 
 type Schema {
   query: Query
diff --git a/graphql/sources/torrent_query.graphql b/graphql/sources/torrent_query.graphql
index f9096f5..6ecd144 100644
--- a/graphql/sources/torrent_query.graphql
+++ b/graphql/sources/torrent_query.graphql
@@ -1,6 +1,7 @@
 type TorrentDaemonQuery {
   torrents(filter: TorrentsFilter): [Torrent!]! @resolver
-  stats: TorrentStats! @resolver
+  clientStats: TorrentClientStats! @resolver
+  statsHistory(since: DateTime!, infohash: String): [TorrentStats!]! @resolver
 }
 
 input TorrentsFilter {
diff --git a/graphql/sources/torrent_types.graphql b/graphql/sources/torrent_types.graphql
index 1b64219..d007e3a 100644
--- a/graphql/sources/torrent_types.graphql
+++ b/graphql/sources/torrent_types.graphql
@@ -33,7 +33,7 @@ enum TorrentPriority {
   NOW
 }
 
-type TorrentStats {
+type TorrentClientStats {
   bytesWritten: Int!
   bytesWrittenData: Int!
   bytesRead: Int!
@@ -51,3 +51,12 @@ type TorrentStats {
   piecesDirtiedGood: Int!
   piecesDirtiedBad: Int!
 }
+
+type TorrentStats {
+  timestamp: DateTime!
+  downloadedBytes: UInt!
+  uploadedBytes: UInt!
+  totalPeers: UInt!
+  activePeers: UInt!
+  connectedSeeders: UInt!
+}
diff --git a/pkg/go-nfs/filesystem.go b/pkg/go-nfs/filesystem.go
index 2207173..22e04c2 100644
--- a/pkg/go-nfs/filesystem.go
+++ b/pkg/go-nfs/filesystem.go
@@ -70,7 +70,7 @@ type File interface {
 	// Name returns the name of the file as presented to Open.
 	Name() string
 	ctxio.Writer
-	ctxio.Reader
+	// ctxio.Reader
 	ctxio.ReaderAt
 	io.Seeker
 	ctxio.Closer
diff --git a/pkg/go-nfs/nfs_test.go b/pkg/go-nfs/nfs_test.go
index e0a9b36..a63c297 100644
--- a/pkg/go-nfs/nfs_test.go
+++ b/pkg/go-nfs/nfs_test.go
@@ -89,7 +89,7 @@ func TestNFS(t *testing.T) {
 	}
 	mf, _ := mem.OpenFile(ctx, "/helloworld.txt", os.O_RDONLY, 0)
 	buf := make([]byte, len(b))
-	if _, err = mf.Read(ctx, buf[:]); err != nil {
+	if _, err = mf.ReadAt(ctx, buf[:], 0); err != nil {
 		t.Fatal(err)
 	}
 	if !bytes.Equal(buf, b) {
diff --git a/src/delivery/api.go b/src/delivery/api.go
deleted file mode 100644
index feab526..0000000
--- a/src/delivery/api.go
+++ /dev/null
@@ -1,126 +0,0 @@
-package delivery
-
-import (
-	"bytes"
-	"io"
-	"math"
-	"net/http"
-	"os"
-
-	"git.kmsign.ru/royalcat/tstor/src/sources/torrent"
-	"github.com/anacrolix/missinggo/v2/filecache"
-	"github.com/gin-gonic/gin"
-)
-
-var apiStatusHandler = func(fc *filecache.Cache, ss *torrent.Stats) gin.HandlerFunc {
-	return func(ctx *gin.Context) {
-		stat := gin.H{
-			"torrentStats": ss.GlobalStats(),
-		}
-
-		if fc != nil {
-			stat["cacheItems"] = fc.Info().NumItems
-			stat["cacheFilled"] = fc.Info().Filled / 1024 / 1024
-			stat["cacheCapacity"] = fc.Info().Capacity / 1024 / 1024
-		}
-
-		// TODO move to a struct
-		ctx.JSON(http.StatusOK, stat)
-	}
-}
-
-// var apiServersHandler = func(ss []*service.Server) gin.HandlerFunc {
-// 	return func(ctx *gin.Context) {
-// 		var infos []*torrent.ServerInfo
-// 		for _, s := range ss {
-// 			infos = append(infos, s.Info())
-// 		}
-// 		ctx.JSON(http.StatusOK, infos)
-// 	}
-// }
-
-// var apiRoutesHandler = func(ss *service.Stats) gin.HandlerFunc {
-// 	return func(ctx *gin.Context) {
-// 		s := ss.RoutesStats()
-// 		sort.Sort(torrent.ByName(s))
-// 		ctx.JSON(http.StatusOK, s)
-// 	}
-// }
-
-// var apiAddTorrentHandler = func(s *service.Service) gin.HandlerFunc {
-// 	return func(ctx *gin.Context) {
-// 		route := ctx.Param("route")
-
-// 		var json RouteAdd
-// 		if err := ctx.ShouldBindJSON(&json); err != nil {
-// 			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-// 			return
-// 		}
-
-// 		if err := s.AddMagnet(route, json.Magnet); err != nil {
-// 			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-// 			return
-// 		}
-
-// 		ctx.JSON(http.StatusOK, nil)
-// 	}
-// }
-
-// var apiDelTorrentHandler = func(s *service.Service) gin.HandlerFunc {
-// 	return func(ctx *gin.Context) {
-// 		route := ctx.Param("route")
-// 		hash := ctx.Param("torrent_hash")
-
-// 		if err := s.RemoveFromHash(route, hash); err != nil {
-// 			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-// 			return
-// 		}
-
-// 		ctx.JSON(http.StatusOK, nil)
-// 	}
-// }
-
-var apiLogHandler = func(path string) gin.HandlerFunc {
-	return func(ctx *gin.Context) {
-		f, err := os.Open(path)
-		if err != nil {
-			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-			return
-		}
-
-		fi, err := f.Stat()
-		if err != nil {
-			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-			return
-		}
-
-		max := math.Max(float64(-fi.Size()), -1024*8*8)
-		_, err = f.Seek(int64(max), io.SeekEnd)
-		if err != nil {
-			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-			return
-		}
-
-		var b bytes.Buffer
-		ctx.Stream(func(w io.Writer) bool {
-			_, err := b.ReadFrom(f)
-			if err != nil {
-				ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-				return false
-			}
-
-			_, err = b.WriteTo(w)
-			if err != nil {
-				ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-				return false
-			}
-
-			return true
-		})
-
-		if err := f.Close(); err != nil {
-			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-			return
-		}
-	}
-}
diff --git a/src/delivery/graphql/generated.go b/src/delivery/graphql/generated.go
index 72d0235..412d25b 100644
--- a/src/delivery/graphql/generated.go
+++ b/src/delivery/graphql/generated.go
@@ -128,6 +128,22 @@ type ComplexityRoot struct {
 		TorrentFilePath func(childComplexity int) int
 	}
 
+	TorrentClientStats struct {
+		BytesRead                   func(childComplexity int) int
+		BytesReadData               func(childComplexity int) int
+		BytesReadUsefulData         func(childComplexity int) int
+		BytesReadUsefulIntendedData func(childComplexity int) int
+		BytesWritten                func(childComplexity int) int
+		BytesWrittenData            func(childComplexity int) int
+		ChunksRead                  func(childComplexity int) int
+		ChunksReadUseful            func(childComplexity int) int
+		ChunksReadWasted            func(childComplexity int) int
+		ChunksWritten               func(childComplexity int) int
+		MetadataChunksRead          func(childComplexity int) int
+		PiecesDirtiedBad            func(childComplexity int) int
+		PiecesDirtiedGood           func(childComplexity int) int
+	}
+
 	TorrentDaemonMutation struct {
 		Cleanup            func(childComplexity int, files *bool, dryRun bool) int
 		SetTorrentPriority func(childComplexity int, infohash string, file *string, priority types.PiecePriority) int
@@ -135,8 +151,9 @@ type ComplexityRoot struct {
 	}
 
 	TorrentDaemonQuery struct {
-		Stats    func(childComplexity int) int
-		Torrents func(childComplexity int, filter *model.TorrentsFilter) int
+		ClientStats  func(childComplexity int) int
+		StatsHistory func(childComplexity int, since time.Time, infohash *string) int
+		Torrents     func(childComplexity int, filter *model.TorrentsFilter) int
 	}
 
 	TorrentFS struct {
@@ -173,19 +190,12 @@ type ComplexityRoot struct {
 	}
 
 	TorrentStats struct {
-		BytesRead                   func(childComplexity int) int
-		BytesReadData               func(childComplexity int) int
-		BytesReadUsefulData         func(childComplexity int) int
-		BytesReadUsefulIntendedData func(childComplexity int) int
-		BytesWritten                func(childComplexity int) int
-		BytesWrittenData            func(childComplexity int) int
-		ChunksRead                  func(childComplexity int) int
-		ChunksReadUseful            func(childComplexity int) int
-		ChunksReadWasted            func(childComplexity int) int
-		ChunksWritten               func(childComplexity int) int
-		MetadataChunksRead          func(childComplexity int) int
-		PiecesDirtiedBad            func(childComplexity int) int
-		PiecesDirtiedGood           func(childComplexity int) int
+		ActivePeers      func(childComplexity int) int
+		ConnectedSeeders func(childComplexity int) int
+		DownloadedBytes  func(childComplexity int) int
+		Timestamp        func(childComplexity int) int
+		TotalPeers       func(childComplexity int) int
+		UploadedBytes    func(childComplexity int) int
 	}
 }
 
@@ -225,7 +235,8 @@ type TorrentDaemonMutationResolver interface {
 }
 type TorrentDaemonQueryResolver interface {
 	Torrents(ctx context.Context, obj *model.TorrentDaemonQuery, filter *model.TorrentsFilter) ([]*model.Torrent, error)
-	Stats(ctx context.Context, obj *model.TorrentDaemonQuery) (*model.TorrentStats, error)
+	ClientStats(ctx context.Context, obj *model.TorrentDaemonQuery) (*model.TorrentClientStats, error)
+	StatsHistory(ctx context.Context, obj *model.TorrentDaemonQuery, since time.Time, infohash *string) ([]*model.TorrentStats, error)
 }
 type TorrentFSResolver interface {
 	Entries(ctx context.Context, obj *model.TorrentFs) ([]model.FsEntry, error)
@@ -485,6 +496,97 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Torrent.TorrentFilePath(childComplexity), true
 
+	case "TorrentClientStats.bytesRead":
+		if e.complexity.TorrentClientStats.BytesRead == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.BytesRead(childComplexity), true
+
+	case "TorrentClientStats.bytesReadData":
+		if e.complexity.TorrentClientStats.BytesReadData == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.BytesReadData(childComplexity), true
+
+	case "TorrentClientStats.bytesReadUsefulData":
+		if e.complexity.TorrentClientStats.BytesReadUsefulData == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.BytesReadUsefulData(childComplexity), true
+
+	case "TorrentClientStats.bytesReadUsefulIntendedData":
+		if e.complexity.TorrentClientStats.BytesReadUsefulIntendedData == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.BytesReadUsefulIntendedData(childComplexity), true
+
+	case "TorrentClientStats.bytesWritten":
+		if e.complexity.TorrentClientStats.BytesWritten == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.BytesWritten(childComplexity), true
+
+	case "TorrentClientStats.bytesWrittenData":
+		if e.complexity.TorrentClientStats.BytesWrittenData == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.BytesWrittenData(childComplexity), true
+
+	case "TorrentClientStats.chunksRead":
+		if e.complexity.TorrentClientStats.ChunksRead == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.ChunksRead(childComplexity), true
+
+	case "TorrentClientStats.chunksReadUseful":
+		if e.complexity.TorrentClientStats.ChunksReadUseful == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.ChunksReadUseful(childComplexity), true
+
+	case "TorrentClientStats.chunksReadWasted":
+		if e.complexity.TorrentClientStats.ChunksReadWasted == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.ChunksReadWasted(childComplexity), true
+
+	case "TorrentClientStats.chunksWritten":
+		if e.complexity.TorrentClientStats.ChunksWritten == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.ChunksWritten(childComplexity), true
+
+	case "TorrentClientStats.metadataChunksRead":
+		if e.complexity.TorrentClientStats.MetadataChunksRead == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.MetadataChunksRead(childComplexity), true
+
+	case "TorrentClientStats.piecesDirtiedBad":
+		if e.complexity.TorrentClientStats.PiecesDirtiedBad == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.PiecesDirtiedBad(childComplexity), true
+
+	case "TorrentClientStats.piecesDirtiedGood":
+		if e.complexity.TorrentClientStats.PiecesDirtiedGood == nil {
+			break
+		}
+
+		return e.complexity.TorrentClientStats.PiecesDirtiedGood(childComplexity), true
+
 	case "TorrentDaemonMutation.cleanup":
 		if e.complexity.TorrentDaemonMutation.Cleanup == nil {
 			break
@@ -521,12 +623,24 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.TorrentDaemonMutation.ValidateTorrent(childComplexity, args["filter"].(model.TorrentFilter)), true
 
-	case "TorrentDaemonQuery.stats":
-		if e.complexity.TorrentDaemonQuery.Stats == nil {
+	case "TorrentDaemonQuery.clientStats":
+		if e.complexity.TorrentDaemonQuery.ClientStats == nil {
 			break
 		}
 
-		return e.complexity.TorrentDaemonQuery.Stats(childComplexity), true
+		return e.complexity.TorrentDaemonQuery.ClientStats(childComplexity), true
+
+	case "TorrentDaemonQuery.statsHistory":
+		if e.complexity.TorrentDaemonQuery.StatsHistory == nil {
+			break
+		}
+
+		args, err := ec.field_TorrentDaemonQuery_statsHistory_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.TorrentDaemonQuery.StatsHistory(childComplexity, args["since"].(time.Time), args["infohash"].(*string)), true
 
 	case "TorrentDaemonQuery.torrents":
 		if e.complexity.TorrentDaemonQuery.Torrents == nil {
@@ -666,96 +780,47 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.TorrentProgress.Total(childComplexity), true
 
-	case "TorrentStats.bytesRead":
-		if e.complexity.TorrentStats.BytesRead == nil {
+	case "TorrentStats.activePeers":
+		if e.complexity.TorrentStats.ActivePeers == nil {
 			break
 		}
 
-		return e.complexity.TorrentStats.BytesRead(childComplexity), true
+		return e.complexity.TorrentStats.ActivePeers(childComplexity), true
 
-	case "TorrentStats.bytesReadData":
-		if e.complexity.TorrentStats.BytesReadData == nil {
+	case "TorrentStats.connectedSeeders":
+		if e.complexity.TorrentStats.ConnectedSeeders == nil {
 			break
 		}
 
-		return e.complexity.TorrentStats.BytesReadData(childComplexity), true
+		return e.complexity.TorrentStats.ConnectedSeeders(childComplexity), true
 
-	case "TorrentStats.bytesReadUsefulData":
-		if e.complexity.TorrentStats.BytesReadUsefulData == nil {
+	case "TorrentStats.downloadedBytes":
+		if e.complexity.TorrentStats.DownloadedBytes == nil {
 			break
 		}
 
-		return e.complexity.TorrentStats.BytesReadUsefulData(childComplexity), true
+		return e.complexity.TorrentStats.DownloadedBytes(childComplexity), true
 
-	case "TorrentStats.bytesReadUsefulIntendedData":
-		if e.complexity.TorrentStats.BytesReadUsefulIntendedData == nil {
+	case "TorrentStats.timestamp":
+		if e.complexity.TorrentStats.Timestamp == nil {
 			break
 		}
 
-		return e.complexity.TorrentStats.BytesReadUsefulIntendedData(childComplexity), true
+		return e.complexity.TorrentStats.Timestamp(childComplexity), true
 
-	case "TorrentStats.bytesWritten":
-		if e.complexity.TorrentStats.BytesWritten == nil {
+	case "TorrentStats.totalPeers":
+		if e.complexity.TorrentStats.TotalPeers == nil {
 			break
 		}
 
-		return e.complexity.TorrentStats.BytesWritten(childComplexity), true
+		return e.complexity.TorrentStats.TotalPeers(childComplexity), true
 
-	case "TorrentStats.bytesWrittenData":
-		if e.complexity.TorrentStats.BytesWrittenData == nil {
+	case "TorrentStats.uploadedBytes":
+		if e.complexity.TorrentStats.UploadedBytes == nil {
 			break
 		}
 
-		return e.complexity.TorrentStats.BytesWrittenData(childComplexity), true
-
-	case "TorrentStats.chunksRead":
-		if e.complexity.TorrentStats.ChunksRead == nil {
-			break
-		}
-
-		return e.complexity.TorrentStats.ChunksRead(childComplexity), true
-
-	case "TorrentStats.chunksReadUseful":
-		if e.complexity.TorrentStats.ChunksReadUseful == nil {
-			break
-		}
-
-		return e.complexity.TorrentStats.ChunksReadUseful(childComplexity), true
-
-	case "TorrentStats.chunksReadWasted":
-		if e.complexity.TorrentStats.ChunksReadWasted == nil {
-			break
-		}
-
-		return e.complexity.TorrentStats.ChunksReadWasted(childComplexity), true
-
-	case "TorrentStats.chunksWritten":
-		if e.complexity.TorrentStats.ChunksWritten == nil {
-			break
-		}
-
-		return e.complexity.TorrentStats.ChunksWritten(childComplexity), true
-
-	case "TorrentStats.metadataChunksRead":
-		if e.complexity.TorrentStats.MetadataChunksRead == nil {
-			break
-		}
-
-		return e.complexity.TorrentStats.MetadataChunksRead(childComplexity), true
-
-	case "TorrentStats.piecesDirtiedBad":
-		if e.complexity.TorrentStats.PiecesDirtiedBad == nil {
-			break
-		}
-
-		return e.complexity.TorrentStats.PiecesDirtiedBad(childComplexity), true
-
-	case "TorrentStats.piecesDirtiedGood":
-		if e.complexity.TorrentStats.PiecesDirtiedGood == nil {
-			break
-		}
-
-		return e.complexity.TorrentStats.PiecesDirtiedGood(childComplexity), true
+		return e.complexity.TorrentStats.UploadedBytes(childComplexity), true
 
 	}
 	return 0, false
@@ -911,6 +976,7 @@ directive @stream on FIELD_DEFINITION
 
 scalar DateTime
 scalar Upload
+scalar UInt
 
 type Schema {
   query: Query
@@ -954,7 +1020,8 @@ type DownloadTorrentResponse {
 `, BuiltIn: false},
 	{Name: "../../../graphql/sources/torrent_query.graphql", Input: `type TorrentDaemonQuery {
   torrents(filter: TorrentsFilter): [Torrent!]! @resolver
-  stats: TorrentStats! @resolver
+  clientStats: TorrentClientStats! @resolver
+  statsHistory(since: DateTime!, infohash: String): [TorrentStats!]! @resolver
 }
 
 input TorrentsFilter {
@@ -1016,7 +1083,7 @@ enum TorrentPriority {
   NOW
 }
 
-type TorrentStats {
+type TorrentClientStats {
   bytesWritten: Int!
   bytesWrittenData: Int!
   bytesRead: Int!
@@ -1034,6 +1101,15 @@ type TorrentStats {
   piecesDirtiedGood: Int!
   piecesDirtiedBad: Int!
 }
+
+type TorrentStats {
+  timestamp: DateTime!
+  downloadedBytes: UInt!
+  uploadedBytes: UInt!
+  totalPeers: UInt!
+  activePeers: UInt!
+  connectedSeeders: UInt!
+}
 `, BuiltIn: false},
 	{Name: "../../../graphql/types/filters.graphql", Input: `input Pagination {
   offset: Int!
@@ -1263,6 +1339,30 @@ func (ec *executionContext) field_TorrentDaemonMutation_validateTorrent_args(ctx
 	return args, nil
 }
 
+func (ec *executionContext) field_TorrentDaemonQuery_statsHistory_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 time.Time
+	if tmp, ok := rawArgs["since"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("since"))
+		arg0, err = ec.unmarshalNDateTime2timeᚐTime(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["since"] = arg0
+	var arg1 *string
+	if tmp, ok := rawArgs["infohash"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("infohash"))
+		arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["infohash"] = arg1
+	return args, nil
+}
+
 func (ec *executionContext) field_TorrentDaemonQuery_torrents_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -1827,8 +1927,10 @@ func (ec *executionContext) fieldContext_Query_torrentDaemon(_ context.Context,
 			switch field.Name {
 			case "torrents":
 				return ec.fieldContext_TorrentDaemonQuery_torrents(ctx, field)
-			case "stats":
-				return ec.fieldContext_TorrentDaemonQuery_stats(ctx, field)
+			case "clientStats":
+				return ec.fieldContext_TorrentDaemonQuery_clientStats(ctx, field)
+			case "statsHistory":
+				return ec.fieldContext_TorrentDaemonQuery_statsHistory(ctx, field)
 			}
 			return nil, fmt.Errorf("no field named %q was found under type TorrentDaemonQuery", field.Name)
 		},
@@ -3080,6 +3182,578 @@ func (ec *executionContext) fieldContext_Torrent_peers(_ context.Context, field
 	return fc, nil
 }
 
+func (ec *executionContext) _TorrentClientStats_bytesWritten(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_bytesWritten(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.BytesWritten, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_bytesWritten(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_bytesWrittenData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_bytesWrittenData(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.BytesWrittenData, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_bytesWrittenData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_bytesRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_bytesRead(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.BytesRead, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_bytesRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_bytesReadData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_bytesReadData(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.BytesReadData, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_bytesReadData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_bytesReadUsefulData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_bytesReadUsefulData(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.BytesReadUsefulData, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_bytesReadUsefulData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_bytesReadUsefulIntendedData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_bytesReadUsefulIntendedData(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.BytesReadUsefulIntendedData, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_bytesReadUsefulIntendedData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_chunksWritten(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_chunksWritten(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.ChunksWritten, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_chunksWritten(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_chunksRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_chunksRead(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.ChunksRead, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_chunksRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_chunksReadUseful(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_chunksReadUseful(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.ChunksReadUseful, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_chunksReadUseful(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_chunksReadWasted(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_chunksReadWasted(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.ChunksReadWasted, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_chunksReadWasted(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_metadataChunksRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_metadataChunksRead(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.MetadataChunksRead, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_metadataChunksRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_piecesDirtiedGood(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_piecesDirtiedGood(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.PiecesDirtiedGood, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_piecesDirtiedGood(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentClientStats_piecesDirtiedBad(ctx context.Context, field graphql.CollectedField, obj *model.TorrentClientStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentClientStats_piecesDirtiedBad(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.PiecesDirtiedBad, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentClientStats_piecesDirtiedBad(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentClientStats",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _TorrentDaemonMutation_validateTorrent(ctx context.Context, field graphql.CollectedField, obj *model.TorrentDaemonMutation) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_TorrentDaemonMutation_validateTorrent(ctx, field)
 	if err != nil {
@@ -3406,8 +4080,8 @@ func (ec *executionContext) fieldContext_TorrentDaemonQuery_torrents(ctx context
 	return fc, nil
 }
 
-func (ec *executionContext) _TorrentDaemonQuery_stats(ctx context.Context, field graphql.CollectedField, obj *model.TorrentDaemonQuery) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentDaemonQuery_stats(ctx, field)
+func (ec *executionContext) _TorrentDaemonQuery_clientStats(ctx context.Context, field graphql.CollectedField, obj *model.TorrentDaemonQuery) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentDaemonQuery_clientStats(ctx, field)
 	if err != nil {
 		return graphql.Null
 	}
@@ -3421,7 +4095,7 @@ func (ec *executionContext) _TorrentDaemonQuery_stats(ctx context.Context, field
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		directive0 := func(rctx context.Context) (interface{}, error) {
 			ctx = rctx // use context from middleware stack in children
-			return ec.resolvers.TorrentDaemonQuery().Stats(rctx, obj)
+			return ec.resolvers.TorrentDaemonQuery().ClientStats(rctx, obj)
 		}
 		directive1 := func(ctx context.Context) (interface{}, error) {
 			if ec.directives.Resolver == nil {
@@ -3437,10 +4111,10 @@ func (ec *executionContext) _TorrentDaemonQuery_stats(ctx context.Context, field
 		if tmp == nil {
 			return nil, nil
 		}
-		if data, ok := tmp.(*model.TorrentStats); ok {
+		if data, ok := tmp.(*model.TorrentClientStats); ok {
 			return data, nil
 		}
-		return nil, fmt.Errorf(`unexpected type %T from directive, should be *git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model.TorrentStats`, tmp)
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be *git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model.TorrentClientStats`, tmp)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -3452,12 +4126,12 @@ func (ec *executionContext) _TorrentDaemonQuery_stats(ctx context.Context, field
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*model.TorrentStats)
+	res := resTmp.(*model.TorrentClientStats)
 	fc.Result = res
-	return ec.marshalNTorrentStats2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentStats(ctx, field.Selections, res)
+	return ec.marshalNTorrentClientStats2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentClientStats(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) fieldContext_TorrentDaemonQuery_stats(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+func (ec *executionContext) fieldContext_TorrentDaemonQuery_clientStats(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
 	fc = &graphql.FieldContext{
 		Object:     "TorrentDaemonQuery",
 		Field:      field,
@@ -3466,35 +4140,124 @@ func (ec *executionContext) fieldContext_TorrentDaemonQuery_stats(_ context.Cont
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
 			switch field.Name {
 			case "bytesWritten":
-				return ec.fieldContext_TorrentStats_bytesWritten(ctx, field)
+				return ec.fieldContext_TorrentClientStats_bytesWritten(ctx, field)
 			case "bytesWrittenData":
-				return ec.fieldContext_TorrentStats_bytesWrittenData(ctx, field)
+				return ec.fieldContext_TorrentClientStats_bytesWrittenData(ctx, field)
 			case "bytesRead":
-				return ec.fieldContext_TorrentStats_bytesRead(ctx, field)
+				return ec.fieldContext_TorrentClientStats_bytesRead(ctx, field)
 			case "bytesReadData":
-				return ec.fieldContext_TorrentStats_bytesReadData(ctx, field)
+				return ec.fieldContext_TorrentClientStats_bytesReadData(ctx, field)
 			case "bytesReadUsefulData":
-				return ec.fieldContext_TorrentStats_bytesReadUsefulData(ctx, field)
+				return ec.fieldContext_TorrentClientStats_bytesReadUsefulData(ctx, field)
 			case "bytesReadUsefulIntendedData":
-				return ec.fieldContext_TorrentStats_bytesReadUsefulIntendedData(ctx, field)
+				return ec.fieldContext_TorrentClientStats_bytesReadUsefulIntendedData(ctx, field)
 			case "chunksWritten":
-				return ec.fieldContext_TorrentStats_chunksWritten(ctx, field)
+				return ec.fieldContext_TorrentClientStats_chunksWritten(ctx, field)
 			case "chunksRead":
-				return ec.fieldContext_TorrentStats_chunksRead(ctx, field)
+				return ec.fieldContext_TorrentClientStats_chunksRead(ctx, field)
 			case "chunksReadUseful":
-				return ec.fieldContext_TorrentStats_chunksReadUseful(ctx, field)
+				return ec.fieldContext_TorrentClientStats_chunksReadUseful(ctx, field)
 			case "chunksReadWasted":
-				return ec.fieldContext_TorrentStats_chunksReadWasted(ctx, field)
+				return ec.fieldContext_TorrentClientStats_chunksReadWasted(ctx, field)
 			case "metadataChunksRead":
-				return ec.fieldContext_TorrentStats_metadataChunksRead(ctx, field)
+				return ec.fieldContext_TorrentClientStats_metadataChunksRead(ctx, field)
 			case "piecesDirtiedGood":
-				return ec.fieldContext_TorrentStats_piecesDirtiedGood(ctx, field)
+				return ec.fieldContext_TorrentClientStats_piecesDirtiedGood(ctx, field)
 			case "piecesDirtiedBad":
-				return ec.fieldContext_TorrentStats_piecesDirtiedBad(ctx, field)
+				return ec.fieldContext_TorrentClientStats_piecesDirtiedBad(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type TorrentClientStats", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentDaemonQuery_statsHistory(ctx context.Context, field graphql.CollectedField, obj *model.TorrentDaemonQuery) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentDaemonQuery_statsHistory(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		directive0 := func(rctx context.Context) (interface{}, error) {
+			ctx = rctx // use context from middleware stack in children
+			return ec.resolvers.TorrentDaemonQuery().StatsHistory(rctx, obj, fc.Args["since"].(time.Time), fc.Args["infohash"].(*string))
+		}
+		directive1 := func(ctx context.Context) (interface{}, error) {
+			if ec.directives.Resolver == nil {
+				return nil, errors.New("directive resolver is not implemented")
+			}
+			return ec.directives.Resolver(ctx, obj, directive0)
+		}
+
+		tmp, err := directive1(rctx)
+		if err != nil {
+			return nil, graphql.ErrorOnPath(ctx, err)
+		}
+		if tmp == nil {
+			return nil, nil
+		}
+		if data, ok := tmp.([]*model.TorrentStats); ok {
+			return data, nil
+		}
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be []*git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model.TorrentStats`, tmp)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]*model.TorrentStats)
+	fc.Result = res
+	return ec.marshalNTorrentStats2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentStatsᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentDaemonQuery_statsHistory(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentDaemonQuery",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "timestamp":
+				return ec.fieldContext_TorrentStats_timestamp(ctx, field)
+			case "downloadedBytes":
+				return ec.fieldContext_TorrentStats_downloadedBytes(ctx, field)
+			case "uploadedBytes":
+				return ec.fieldContext_TorrentStats_uploadedBytes(ctx, field)
+			case "totalPeers":
+				return ec.fieldContext_TorrentStats_totalPeers(ctx, field)
+			case "activePeers":
+				return ec.fieldContext_TorrentStats_activePeers(ctx, field)
+			case "connectedSeeders":
+				return ec.fieldContext_TorrentStats_connectedSeeders(ctx, field)
 			}
 			return nil, fmt.Errorf("no field named %q was found under type TorrentStats", field.Name)
 		},
 	}
+	defer func() {
+		if r := recover(); r != nil {
+			err = ec.Recover(ctx, r)
+			ec.Error(ctx, err)
+		}
+	}()
+	ctx = graphql.WithFieldContext(ctx, fc)
+	if fc.Args, err = ec.field_TorrentDaemonQuery_statsHistory_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
 	return fc, nil
 }
 
@@ -4390,8 +5153,8 @@ func (ec *executionContext) fieldContext_TorrentProgress_total(_ context.Context
 	return fc, nil
 }
 
-func (ec *executionContext) _TorrentStats_bytesWritten(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_bytesWritten(ctx, field)
+func (ec *executionContext) _TorrentStats_timestamp(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentStats_timestamp(ctx, field)
 	if err != nil {
 		return graphql.Null
 	}
@@ -4404,7 +5167,7 @@ func (ec *executionContext) _TorrentStats_bytesWritten(ctx context.Context, fiel
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.BytesWritten, nil
+		return obj.Timestamp, nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -4416,26 +5179,26 @@ func (ec *executionContext) _TorrentStats_bytesWritten(ctx context.Context, fiel
 		}
 		return graphql.Null
 	}
-	res := resTmp.(int64)
+	res := resTmp.(time.Time)
 	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
+	return ec.marshalNDateTime2timeᚐTime(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) fieldContext_TorrentStats_bytesWritten(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+func (ec *executionContext) fieldContext_TorrentStats_timestamp(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
 	fc = &graphql.FieldContext{
 		Object:     "TorrentStats",
 		Field:      field,
 		IsMethod:   false,
 		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
+			return nil, errors.New("field of type DateTime does not have child fields")
 		},
 	}
 	return fc, nil
 }
 
-func (ec *executionContext) _TorrentStats_bytesWrittenData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_bytesWrittenData(ctx, field)
+func (ec *executionContext) _TorrentStats_downloadedBytes(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentStats_downloadedBytes(ctx, field)
 	if err != nil {
 		return graphql.Null
 	}
@@ -4448,7 +5211,7 @@ func (ec *executionContext) _TorrentStats_bytesWrittenData(ctx context.Context,
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.BytesWrittenData, nil
+		return obj.DownloadedBytes, nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -4460,26 +5223,26 @@ func (ec *executionContext) _TorrentStats_bytesWrittenData(ctx context.Context,
 		}
 		return graphql.Null
 	}
-	res := resTmp.(int64)
+	res := resTmp.(uint)
 	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
+	return ec.marshalNUInt2uint(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) fieldContext_TorrentStats_bytesWrittenData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+func (ec *executionContext) fieldContext_TorrentStats_downloadedBytes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
 	fc = &graphql.FieldContext{
 		Object:     "TorrentStats",
 		Field:      field,
 		IsMethod:   false,
 		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
+			return nil, errors.New("field of type UInt does not have child fields")
 		},
 	}
 	return fc, nil
 }
 
-func (ec *executionContext) _TorrentStats_bytesRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_bytesRead(ctx, field)
+func (ec *executionContext) _TorrentStats_uploadedBytes(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentStats_uploadedBytes(ctx, field)
 	if err != nil {
 		return graphql.Null
 	}
@@ -4492,7 +5255,7 @@ func (ec *executionContext) _TorrentStats_bytesRead(ctx context.Context, field g
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.BytesRead, nil
+		return obj.UploadedBytes, nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -4504,26 +5267,26 @@ func (ec *executionContext) _TorrentStats_bytesRead(ctx context.Context, field g
 		}
 		return graphql.Null
 	}
-	res := resTmp.(int64)
+	res := resTmp.(uint)
 	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
+	return ec.marshalNUInt2uint(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) fieldContext_TorrentStats_bytesRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+func (ec *executionContext) fieldContext_TorrentStats_uploadedBytes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
 	fc = &graphql.FieldContext{
 		Object:     "TorrentStats",
 		Field:      field,
 		IsMethod:   false,
 		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
+			return nil, errors.New("field of type UInt does not have child fields")
 		},
 	}
 	return fc, nil
 }
 
-func (ec *executionContext) _TorrentStats_bytesReadData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_bytesReadData(ctx, field)
+func (ec *executionContext) _TorrentStats_totalPeers(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentStats_totalPeers(ctx, field)
 	if err != nil {
 		return graphql.Null
 	}
@@ -4536,7 +5299,7 @@ func (ec *executionContext) _TorrentStats_bytesReadData(ctx context.Context, fie
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.BytesReadData, nil
+		return obj.TotalPeers, nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -4548,26 +5311,26 @@ func (ec *executionContext) _TorrentStats_bytesReadData(ctx context.Context, fie
 		}
 		return graphql.Null
 	}
-	res := resTmp.(int64)
+	res := resTmp.(uint)
 	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
+	return ec.marshalNUInt2uint(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) fieldContext_TorrentStats_bytesReadData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+func (ec *executionContext) fieldContext_TorrentStats_totalPeers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
 	fc = &graphql.FieldContext{
 		Object:     "TorrentStats",
 		Field:      field,
 		IsMethod:   false,
 		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
+			return nil, errors.New("field of type UInt does not have child fields")
 		},
 	}
 	return fc, nil
 }
 
-func (ec *executionContext) _TorrentStats_bytesReadUsefulData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_bytesReadUsefulData(ctx, field)
+func (ec *executionContext) _TorrentStats_activePeers(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentStats_activePeers(ctx, field)
 	if err != nil {
 		return graphql.Null
 	}
@@ -4580,7 +5343,7 @@ func (ec *executionContext) _TorrentStats_bytesReadUsefulData(ctx context.Contex
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.BytesReadUsefulData, nil
+		return obj.ActivePeers, nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -4592,26 +5355,26 @@ func (ec *executionContext) _TorrentStats_bytesReadUsefulData(ctx context.Contex
 		}
 		return graphql.Null
 	}
-	res := resTmp.(int64)
+	res := resTmp.(uint)
 	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
+	return ec.marshalNUInt2uint(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) fieldContext_TorrentStats_bytesReadUsefulData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+func (ec *executionContext) fieldContext_TorrentStats_activePeers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
 	fc = &graphql.FieldContext{
 		Object:     "TorrentStats",
 		Field:      field,
 		IsMethod:   false,
 		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
+			return nil, errors.New("field of type UInt does not have child fields")
 		},
 	}
 	return fc, nil
 }
 
-func (ec *executionContext) _TorrentStats_bytesReadUsefulIntendedData(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_bytesReadUsefulIntendedData(ctx, field)
+func (ec *executionContext) _TorrentStats_connectedSeeders(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentStats_connectedSeeders(ctx, field)
 	if err != nil {
 		return graphql.Null
 	}
@@ -4624,7 +5387,7 @@ func (ec *executionContext) _TorrentStats_bytesReadUsefulIntendedData(ctx contex
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.BytesReadUsefulIntendedData, nil
+		return obj.ConnectedSeeders, nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -4636,327 +5399,19 @@ func (ec *executionContext) _TorrentStats_bytesReadUsefulIntendedData(ctx contex
 		}
 		return graphql.Null
 	}
-	res := resTmp.(int64)
+	res := resTmp.(uint)
 	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
+	return ec.marshalNUInt2uint(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) fieldContext_TorrentStats_bytesReadUsefulIntendedData(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+func (ec *executionContext) fieldContext_TorrentStats_connectedSeeders(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
 	fc = &graphql.FieldContext{
 		Object:     "TorrentStats",
 		Field:      field,
 		IsMethod:   false,
 		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
-		},
-	}
-	return fc, nil
-}
-
-func (ec *executionContext) _TorrentStats_chunksWritten(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_chunksWritten(ctx, field)
-	if err != nil {
-		return graphql.Null
-	}
-	ctx = graphql.WithFieldContext(ctx, fc)
-	defer func() {
-		if r := recover(); r != nil {
-			ec.Error(ctx, ec.Recover(ctx, r))
-			ret = graphql.Null
-		}
-	}()
-	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return obj.ChunksWritten, nil
-	})
-	if err != nil {
-		ec.Error(ctx, err)
-		return graphql.Null
-	}
-	if resTmp == nil {
-		if !graphql.HasFieldError(ctx, fc) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(int64)
-	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
-}
-
-func (ec *executionContext) fieldContext_TorrentStats_chunksWritten(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
-	fc = &graphql.FieldContext{
-		Object:     "TorrentStats",
-		Field:      field,
-		IsMethod:   false,
-		IsResolver: false,
-		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
-		},
-	}
-	return fc, nil
-}
-
-func (ec *executionContext) _TorrentStats_chunksRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_chunksRead(ctx, field)
-	if err != nil {
-		return graphql.Null
-	}
-	ctx = graphql.WithFieldContext(ctx, fc)
-	defer func() {
-		if r := recover(); r != nil {
-			ec.Error(ctx, ec.Recover(ctx, r))
-			ret = graphql.Null
-		}
-	}()
-	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return obj.ChunksRead, nil
-	})
-	if err != nil {
-		ec.Error(ctx, err)
-		return graphql.Null
-	}
-	if resTmp == nil {
-		if !graphql.HasFieldError(ctx, fc) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(int64)
-	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
-}
-
-func (ec *executionContext) fieldContext_TorrentStats_chunksRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
-	fc = &graphql.FieldContext{
-		Object:     "TorrentStats",
-		Field:      field,
-		IsMethod:   false,
-		IsResolver: false,
-		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
-		},
-	}
-	return fc, nil
-}
-
-func (ec *executionContext) _TorrentStats_chunksReadUseful(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_chunksReadUseful(ctx, field)
-	if err != nil {
-		return graphql.Null
-	}
-	ctx = graphql.WithFieldContext(ctx, fc)
-	defer func() {
-		if r := recover(); r != nil {
-			ec.Error(ctx, ec.Recover(ctx, r))
-			ret = graphql.Null
-		}
-	}()
-	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return obj.ChunksReadUseful, nil
-	})
-	if err != nil {
-		ec.Error(ctx, err)
-		return graphql.Null
-	}
-	if resTmp == nil {
-		if !graphql.HasFieldError(ctx, fc) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(int64)
-	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
-}
-
-func (ec *executionContext) fieldContext_TorrentStats_chunksReadUseful(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
-	fc = &graphql.FieldContext{
-		Object:     "TorrentStats",
-		Field:      field,
-		IsMethod:   false,
-		IsResolver: false,
-		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
-		},
-	}
-	return fc, nil
-}
-
-func (ec *executionContext) _TorrentStats_chunksReadWasted(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_chunksReadWasted(ctx, field)
-	if err != nil {
-		return graphql.Null
-	}
-	ctx = graphql.WithFieldContext(ctx, fc)
-	defer func() {
-		if r := recover(); r != nil {
-			ec.Error(ctx, ec.Recover(ctx, r))
-			ret = graphql.Null
-		}
-	}()
-	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return obj.ChunksReadWasted, nil
-	})
-	if err != nil {
-		ec.Error(ctx, err)
-		return graphql.Null
-	}
-	if resTmp == nil {
-		if !graphql.HasFieldError(ctx, fc) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(int64)
-	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
-}
-
-func (ec *executionContext) fieldContext_TorrentStats_chunksReadWasted(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
-	fc = &graphql.FieldContext{
-		Object:     "TorrentStats",
-		Field:      field,
-		IsMethod:   false,
-		IsResolver: false,
-		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
-		},
-	}
-	return fc, nil
-}
-
-func (ec *executionContext) _TorrentStats_metadataChunksRead(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_metadataChunksRead(ctx, field)
-	if err != nil {
-		return graphql.Null
-	}
-	ctx = graphql.WithFieldContext(ctx, fc)
-	defer func() {
-		if r := recover(); r != nil {
-			ec.Error(ctx, ec.Recover(ctx, r))
-			ret = graphql.Null
-		}
-	}()
-	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return obj.MetadataChunksRead, nil
-	})
-	if err != nil {
-		ec.Error(ctx, err)
-		return graphql.Null
-	}
-	if resTmp == nil {
-		if !graphql.HasFieldError(ctx, fc) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(int64)
-	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
-}
-
-func (ec *executionContext) fieldContext_TorrentStats_metadataChunksRead(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
-	fc = &graphql.FieldContext{
-		Object:     "TorrentStats",
-		Field:      field,
-		IsMethod:   false,
-		IsResolver: false,
-		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
-		},
-	}
-	return fc, nil
-}
-
-func (ec *executionContext) _TorrentStats_piecesDirtiedGood(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_piecesDirtiedGood(ctx, field)
-	if err != nil {
-		return graphql.Null
-	}
-	ctx = graphql.WithFieldContext(ctx, fc)
-	defer func() {
-		if r := recover(); r != nil {
-			ec.Error(ctx, ec.Recover(ctx, r))
-			ret = graphql.Null
-		}
-	}()
-	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return obj.PiecesDirtiedGood, nil
-	})
-	if err != nil {
-		ec.Error(ctx, err)
-		return graphql.Null
-	}
-	if resTmp == nil {
-		if !graphql.HasFieldError(ctx, fc) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(int64)
-	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
-}
-
-func (ec *executionContext) fieldContext_TorrentStats_piecesDirtiedGood(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
-	fc = &graphql.FieldContext{
-		Object:     "TorrentStats",
-		Field:      field,
-		IsMethod:   false,
-		IsResolver: false,
-		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
-		},
-	}
-	return fc, nil
-}
-
-func (ec *executionContext) _TorrentStats_piecesDirtiedBad(ctx context.Context, field graphql.CollectedField, obj *model.TorrentStats) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_TorrentStats_piecesDirtiedBad(ctx, field)
-	if err != nil {
-		return graphql.Null
-	}
-	ctx = graphql.WithFieldContext(ctx, fc)
-	defer func() {
-		if r := recover(); r != nil {
-			ec.Error(ctx, ec.Recover(ctx, r))
-			ret = graphql.Null
-		}
-	}()
-	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return obj.PiecesDirtiedBad, nil
-	})
-	if err != nil {
-		ec.Error(ctx, err)
-		return graphql.Null
-	}
-	if resTmp == nil {
-		if !graphql.HasFieldError(ctx, fc) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(int64)
-	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
-}
-
-func (ec *executionContext) fieldContext_TorrentStats_piecesDirtiedBad(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
-	fc = &graphql.FieldContext{
-		Object:     "TorrentStats",
-		Field:      field,
-		IsMethod:   false,
-		IsResolver: false,
-		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
+			return nil, errors.New("field of type UInt does not have child fields")
 		},
 	}
 	return fc, nil
@@ -8418,6 +8873,105 @@ func (ec *executionContext) _Torrent(ctx context.Context, sel ast.SelectionSet,
 	return out
 }
 
+var torrentClientStatsImplementors = []string{"TorrentClientStats"}
+
+func (ec *executionContext) _TorrentClientStats(ctx context.Context, sel ast.SelectionSet, obj *model.TorrentClientStats) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, torrentClientStatsImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("TorrentClientStats")
+		case "bytesWritten":
+			out.Values[i] = ec._TorrentClientStats_bytesWritten(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "bytesWrittenData":
+			out.Values[i] = ec._TorrentClientStats_bytesWrittenData(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "bytesRead":
+			out.Values[i] = ec._TorrentClientStats_bytesRead(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "bytesReadData":
+			out.Values[i] = ec._TorrentClientStats_bytesReadData(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "bytesReadUsefulData":
+			out.Values[i] = ec._TorrentClientStats_bytesReadUsefulData(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "bytesReadUsefulIntendedData":
+			out.Values[i] = ec._TorrentClientStats_bytesReadUsefulIntendedData(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "chunksWritten":
+			out.Values[i] = ec._TorrentClientStats_chunksWritten(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "chunksRead":
+			out.Values[i] = ec._TorrentClientStats_chunksRead(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "chunksReadUseful":
+			out.Values[i] = ec._TorrentClientStats_chunksReadUseful(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "chunksReadWasted":
+			out.Values[i] = ec._TorrentClientStats_chunksReadWasted(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "metadataChunksRead":
+			out.Values[i] = ec._TorrentClientStats_metadataChunksRead(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "piecesDirtiedGood":
+			out.Values[i] = ec._TorrentClientStats_piecesDirtiedGood(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "piecesDirtiedBad":
+			out.Values[i] = ec._TorrentClientStats_piecesDirtiedBad(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
 var torrentDaemonMutationImplementors = []string{"TorrentDaemonMutation"}
 
 func (ec *executionContext) _TorrentDaemonMutation(ctx context.Context, sel ast.SelectionSet, obj *model.TorrentDaemonMutation) graphql.Marshaler {
@@ -8607,7 +9161,7 @@ func (ec *executionContext) _TorrentDaemonQuery(ctx context.Context, sel ast.Sel
 			}
 
 			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
-		case "stats":
+		case "clientStats":
 			field := field
 
 			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
@@ -8616,7 +9170,43 @@ func (ec *executionContext) _TorrentDaemonQuery(ctx context.Context, sel ast.Sel
 						ec.Error(ctx, ec.Recover(ctx, r))
 					}
 				}()
-				res = ec._TorrentDaemonQuery_stats(ctx, field, obj)
+				res = ec._TorrentDaemonQuery_clientStats(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&fs.Invalids, 1)
+				}
+				return res
+			}
+
+			if field.Deferrable != nil {
+				dfs, ok := deferred[field.Deferrable.Label]
+				di := 0
+				if ok {
+					dfs.AddField(field)
+					di = len(dfs.Values) - 1
+				} else {
+					dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+					deferred[field.Deferrable.Label] = dfs
+				}
+				dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+					return innerFunc(ctx, dfs)
+				})
+
+				// don't run the out.Concurrently() call below
+				out.Values[i] = graphql.Null
+				continue
+			}
+
+			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+		case "statsHistory":
+			field := field
+
+			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._TorrentDaemonQuery_statsHistory(ctx, field, obj)
 				if res == graphql.Null {
 					atomic.AddUint32(&fs.Invalids, 1)
 				}
@@ -8999,68 +9589,33 @@ func (ec *executionContext) _TorrentStats(ctx context.Context, sel ast.Selection
 		switch field.Name {
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("TorrentStats")
-		case "bytesWritten":
-			out.Values[i] = ec._TorrentStats_bytesWritten(ctx, field, obj)
+		case "timestamp":
+			out.Values[i] = ec._TorrentStats_timestamp(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				out.Invalids++
 			}
-		case "bytesWrittenData":
-			out.Values[i] = ec._TorrentStats_bytesWrittenData(ctx, field, obj)
+		case "downloadedBytes":
+			out.Values[i] = ec._TorrentStats_downloadedBytes(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				out.Invalids++
 			}
-		case "bytesRead":
-			out.Values[i] = ec._TorrentStats_bytesRead(ctx, field, obj)
+		case "uploadedBytes":
+			out.Values[i] = ec._TorrentStats_uploadedBytes(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				out.Invalids++
 			}
-		case "bytesReadData":
-			out.Values[i] = ec._TorrentStats_bytesReadData(ctx, field, obj)
+		case "totalPeers":
+			out.Values[i] = ec._TorrentStats_totalPeers(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				out.Invalids++
 			}
-		case "bytesReadUsefulData":
-			out.Values[i] = ec._TorrentStats_bytesReadUsefulData(ctx, field, obj)
+		case "activePeers":
+			out.Values[i] = ec._TorrentStats_activePeers(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				out.Invalids++
 			}
-		case "bytesReadUsefulIntendedData":
-			out.Values[i] = ec._TorrentStats_bytesReadUsefulIntendedData(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				out.Invalids++
-			}
-		case "chunksWritten":
-			out.Values[i] = ec._TorrentStats_chunksWritten(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				out.Invalids++
-			}
-		case "chunksRead":
-			out.Values[i] = ec._TorrentStats_chunksRead(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				out.Invalids++
-			}
-		case "chunksReadUseful":
-			out.Values[i] = ec._TorrentStats_chunksReadUseful(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				out.Invalids++
-			}
-		case "chunksReadWasted":
-			out.Values[i] = ec._TorrentStats_chunksReadWasted(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				out.Invalids++
-			}
-		case "metadataChunksRead":
-			out.Values[i] = ec._TorrentStats_metadataChunksRead(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				out.Invalids++
-			}
-		case "piecesDirtiedGood":
-			out.Values[i] = ec._TorrentStats_piecesDirtiedGood(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				out.Invalids++
-			}
-		case "piecesDirtiedBad":
-			out.Values[i] = ec._TorrentStats_piecesDirtiedBad(ctx, field, obj)
+		case "connectedSeeders":
+			out.Values[i] = ec._TorrentStats_connectedSeeders(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				out.Invalids++
 			}
@@ -9438,6 +9993,21 @@ func (ec *executionContext) marshalNCleanupResponse2ᚖgitᚗkmsignᚗruᚋroyal
 	return ec._CleanupResponse(ctx, sel, v)
 }
 
+func (ec *executionContext) unmarshalNDateTime2timeᚐTime(ctx context.Context, v interface{}) (time.Time, error) {
+	res, err := graphql.UnmarshalTime(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNDateTime2timeᚐTime(ctx context.Context, sel ast.SelectionSet, v time.Time) graphql.Marshaler {
+	res := graphql.MarshalTime(v)
+	if res == graphql.Null {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+	}
+	return res
+}
+
 func (ec *executionContext) unmarshalNFloat2float64(ctx context.Context, v interface{}) (float64, error) {
 	res, err := graphql.UnmarshalFloatContext(ctx, v)
 	return res, graphql.ErrorOnPath(ctx, err)
@@ -9638,6 +10208,16 @@ func (ec *executionContext) marshalNTorrent2ᚖgitᚗkmsignᚗruᚋroyalcatᚋts
 	return ec._Torrent(ctx, sel, v)
 }
 
+func (ec *executionContext) marshalNTorrentClientStats2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentClientStats(ctx context.Context, sel ast.SelectionSet, v *model.TorrentClientStats) graphql.Marshaler {
+	if v == nil {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+		return graphql.Null
+	}
+	return ec._TorrentClientStats(ctx, sel, v)
+}
+
 func (ec *executionContext) marshalNTorrentFile2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentFileᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.TorrentFile) graphql.Marshaler {
 	ret := make(graphql.Array, len(v))
 	var wg sync.WaitGroup
@@ -9784,6 +10364,50 @@ var (
 	}
 )
 
+func (ec *executionContext) marshalNTorrentStats2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentStatsᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.TorrentStats) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalNTorrentStats2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentStats(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
 func (ec *executionContext) marshalNTorrentStats2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentStats(ctx context.Context, sel ast.SelectionSet, v *model.TorrentStats) graphql.Marshaler {
 	if v == nil {
 		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
@@ -9794,6 +10418,21 @@ func (ec *executionContext) marshalNTorrentStats2ᚖgitᚗkmsignᚗruᚋroyalcat
 	return ec._TorrentStats(ctx, sel, v)
 }
 
+func (ec *executionContext) unmarshalNUInt2uint(ctx context.Context, v interface{}) (uint, error) {
+	res, err := graphql.UnmarshalUint(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNUInt2uint(ctx context.Context, sel ast.SelectionSet, v uint) graphql.Marshaler {
+	res := graphql.MarshalUint(v)
+	if res == graphql.Null {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+	}
+	return res
+}
+
 func (ec *executionContext) unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, v interface{}) (graphql.Upload, error) {
 	res, err := graphql.UnmarshalUpload(v)
 	return res, graphql.ErrorOnPath(ctx, err)
diff --git a/src/delivery/graphql/model/mappers.go b/src/delivery/graphql/model/mappers.go
index 217e171..b3ea223 100644
--- a/src/delivery/graphql/model/mappers.go
+++ b/src/delivery/graphql/model/mappers.go
@@ -7,6 +7,14 @@ import (
 	atorrent "github.com/anacrolix/torrent"
 )
 
+func Apply[I any, O any](in []I, f func(I) O) []O {
+	out := make([]O, len(in))
+	for i, v := range in {
+		out[i] = f(v)
+	}
+	return out
+}
+
 func MapPeerSource(source atorrent.PeerSource) string {
 	switch source {
 	case atorrent.PeerSourceDirect:
@@ -43,3 +51,14 @@ func MapTorrent(ctx context.Context, t *torrent.Controller) (*Torrent, error) {
 		T:              t,
 	}, nil
 }
+
+func MapTorrentStats(s torrent.TorrentStats) *TorrentStats {
+	return &TorrentStats{
+		Timestamp:        s.Timestamp,
+		DownloadedBytes:  uint(s.DownloadedBytes),
+		UploadedBytes:    uint(s.UploadedBytes),
+		TotalPeers:       uint(s.TotalPeers),
+		ActivePeers:      uint(s.ActivePeers),
+		ConnectedSeeders: uint(s.ConnectedSeeders),
+	}
+}
diff --git a/src/delivery/graphql/model/models_gen.go b/src/delivery/graphql/model/models_gen.go
index d1fa34f..7a6e02f 100644
--- a/src/delivery/graphql/model/models_gen.go
+++ b/src/delivery/graphql/model/models_gen.go
@@ -184,6 +184,22 @@ type Torrent struct {
 	T               *torrent.Controller `json:"-"`
 }
 
+type TorrentClientStats struct {
+	BytesWritten                int64 `json:"bytesWritten"`
+	BytesWrittenData            int64 `json:"bytesWrittenData"`
+	BytesRead                   int64 `json:"bytesRead"`
+	BytesReadData               int64 `json:"bytesReadData"`
+	BytesReadUsefulData         int64 `json:"bytesReadUsefulData"`
+	BytesReadUsefulIntendedData int64 `json:"bytesReadUsefulIntendedData"`
+	ChunksWritten               int64 `json:"chunksWritten"`
+	ChunksRead                  int64 `json:"chunksRead"`
+	ChunksReadUseful            int64 `json:"chunksReadUseful"`
+	ChunksReadWasted            int64 `json:"chunksReadWasted"`
+	MetadataChunksRead          int64 `json:"metadataChunksRead"`
+	PiecesDirtiedGood           int64 `json:"piecesDirtiedGood"`
+	PiecesDirtiedBad            int64 `json:"piecesDirtiedBad"`
+}
+
 type TorrentDaemonMutation struct {
 	ValidateTorrent    bool             `json:"validateTorrent"`
 	SetTorrentPriority bool             `json:"setTorrentPriority"`
@@ -191,8 +207,9 @@ type TorrentDaemonMutation struct {
 }
 
 type TorrentDaemonQuery struct {
-	Torrents []*Torrent    `json:"torrents"`
-	Stats    *TorrentStats `json:"stats"`
+	Torrents     []*Torrent          `json:"torrents"`
+	ClientStats  *TorrentClientStats `json:"clientStats"`
+	StatsHistory []*TorrentStats     `json:"statsHistory"`
 }
 
 type TorrentFs struct {
@@ -271,19 +288,12 @@ func (this TorrentProgress) GetCurrent() int64 { return this.Current }
 func (this TorrentProgress) GetTotal() int64   { return this.Total }
 
 type TorrentStats struct {
-	BytesWritten                int64 `json:"bytesWritten"`
-	BytesWrittenData            int64 `json:"bytesWrittenData"`
-	BytesRead                   int64 `json:"bytesRead"`
-	BytesReadData               int64 `json:"bytesReadData"`
-	BytesReadUsefulData         int64 `json:"bytesReadUsefulData"`
-	BytesReadUsefulIntendedData int64 `json:"bytesReadUsefulIntendedData"`
-	ChunksWritten               int64 `json:"chunksWritten"`
-	ChunksRead                  int64 `json:"chunksRead"`
-	ChunksReadUseful            int64 `json:"chunksReadUseful"`
-	ChunksReadWasted            int64 `json:"chunksReadWasted"`
-	MetadataChunksRead          int64 `json:"metadataChunksRead"`
-	PiecesDirtiedGood           int64 `json:"piecesDirtiedGood"`
-	PiecesDirtiedBad            int64 `json:"piecesDirtiedBad"`
+	Timestamp        time.Time `json:"timestamp"`
+	DownloadedBytes  uint      `json:"downloadedBytes"`
+	UploadedBytes    uint      `json:"uploadedBytes"`
+	TotalPeers       uint      `json:"totalPeers"`
+	ActivePeers      uint      `json:"activePeers"`
+	ConnectedSeeders uint      `json:"connectedSeeders"`
 }
 
 type TorrentsFilter struct {
diff --git a/src/delivery/graphql/resolver/torrent_query.resolvers.go b/src/delivery/graphql/resolver/torrent_query.resolvers.go
index 2c77a32..081b45b 100644
--- a/src/delivery/graphql/resolver/torrent_query.resolvers.go
+++ b/src/delivery/graphql/resolver/torrent_query.resolvers.go
@@ -8,10 +8,13 @@ import (
 	"context"
 	"slices"
 	"strings"
+	"time"
 
 	graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql"
 	"git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model"
 	"git.kmsign.ru/royalcat/tstor/src/sources/torrent"
+
+	tinfohash "github.com/anacrolix/torrent/types/infohash"
 )
 
 // Torrents is the resolver for the torrents field.
@@ -86,10 +89,10 @@ func (r *torrentDaemonQueryResolver) Torrents(ctx context.Context, obj *model.To
 	return tr, nil
 }
 
-// Stats is the resolver for the stats field.
-func (r *torrentDaemonQueryResolver) Stats(ctx context.Context, obj *model.TorrentDaemonQuery) (*model.TorrentStats, error) {
+// ClientStats is the resolver for the clientStats field.
+func (r *torrentDaemonQueryResolver) ClientStats(ctx context.Context, obj *model.TorrentDaemonQuery) (*model.TorrentClientStats, error) {
 	stats := r.Service.Stats()
-	return &model.TorrentStats{
+	return &model.TorrentClientStats{
 		BytesWritten:                stats.BytesWritten.Int64(),
 		BytesRead:                   stats.BytesRead.Int64(),
 		BytesWrittenData:            stats.BytesWrittenData.Int64(),
@@ -106,6 +109,33 @@ func (r *torrentDaemonQueryResolver) Stats(ctx context.Context, obj *model.Torre
 	}, nil
 }
 
+// StatsHistory is the resolver for the statsHistory field.
+func (r *torrentDaemonQueryResolver) StatsHistory(ctx context.Context, obj *model.TorrentDaemonQuery, since time.Time, infohash *string) ([]*model.TorrentStats, error) {
+	var stats []torrent.TorrentStats
+	if infohash == nil {
+		stats, err := r.Service.StatsHistory(ctx, since)
+		if err != nil {
+			return nil, err
+		}
+		return model.Apply(stats, model.MapTorrentStats), nil
+	} else if *infohash == "total" {
+		var err error
+		stats, err = r.Service.TotalStatsHistory(ctx, since)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		ih := tinfohash.FromHexString(*infohash)
+		var err error
+		stats, err = r.Service.TorrentStatsHistory(ctx, since, ih)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return model.Apply(stats, model.MapTorrentStats), nil
+}
+
 // TorrentDaemonQuery returns graph.TorrentDaemonQueryResolver implementation.
 func (r *Resolver) TorrentDaemonQuery() graph.TorrentDaemonQueryResolver {
 	return &torrentDaemonQueryResolver{r}
diff --git a/src/sources/torrent/client.go b/src/sources/torrent/client.go
index 0eb0c41..9152a83 100644
--- a/src/sources/torrent/client.go
+++ b/src/sources/torrent/client.go
@@ -2,7 +2,6 @@ package torrent
 
 import (
 	"log/slog"
-	"time"
 
 	"github.com/anacrolix/dht/v2"
 	"github.com/anacrolix/dht/v2/bep44"
@@ -59,9 +58,14 @@ func newClient(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentClient
 	torrentCfg.PeriodicallyAnnounceTorrentsToDht = true
 	torrentCfg.ConfigureAnacrolixDhtServer = func(cfg *dht.ServerConfig) {
 		cfg.Store = fis
-		cfg.Exp = 2 * time.Hour
+		cfg.Exp = dhtTTL
 		cfg.NoSecurity = false
 	}
 
-	return torrent.NewClient(torrentCfg)
+	t, err := torrent.NewClient(torrentCfg)
+	if err != nil {
+		return nil, err
+	}
+
+	return t, nil
 }
diff --git a/src/sources/torrent/daemon.go b/src/sources/torrent/daemon.go
index b33df21..4074dcd 100644
--- a/src/sources/torrent/daemon.go
+++ b/src/sources/torrent/daemon.go
@@ -48,6 +48,7 @@ type Daemon struct {
 	fis            *dhtFileItemStore
 	dirsAquire     kv.Store[string, DirAquire]
 	fileProperties kv.Store[string, FileProperties]
+	statsStore     *statsStore
 
 	loadMutex     sync.Mutex
 	torrentLoaded chan struct{}
@@ -57,6 +58,8 @@ type Daemon struct {
 	log *rlog.Logger
 }
 
+const dhtTTL = 24 * time.Hour
+
 func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon, error) {
 	s := &Daemon{
 		log:           rlog.Component("torrent-service"),
@@ -70,7 +73,7 @@ func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon,
 		return nil, fmt.Errorf("error creating metadata folder: %w", err)
 	}
 
-	s.fis, err = newDHTStore(filepath.Join(conf.MetadataFolder, "dht-item-store"), 3*time.Hour)
+	s.fis, err = newDHTStore(filepath.Join(conf.MetadataFolder, "dht-item-store"), dhtTTL)
 	if err != nil {
 		return nil, fmt.Errorf("error starting item store: %w", err)
 	}
@@ -86,7 +89,6 @@ func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon,
 	}
 
 	s.infoBytes, err = newInfoBytesStore(conf.MetadataFolder)
-
 	if err != nil {
 		return nil, err
 	}
@@ -96,6 +98,11 @@ func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon,
 		return nil, fmt.Errorf("error creating node ID: %w", err)
 	}
 
+	s.statsStore, err = newStatsStore(conf.MetadataFolder, time.Hour*24*30)
+	if err != nil {
+		return nil, err
+	}
+
 	s.client, err = newClient(s.Storage, s.fis, &conf, id)
 	if err != nil {
 		return nil, fmt.Errorf("error starting torrent client: %w", err)
@@ -116,9 +123,77 @@ func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon,
 		close(s.torrentLoaded)
 	}()
 
+	go func() {
+		const period = time.Second * 10
+
+		<-s.torrentLoaded
+
+		timer := time.NewTicker(period)
+		for {
+			select {
+			case <-s.client.Closed():
+				return
+			case <-timer.C:
+				s.updateStats(context.Background())
+			}
+		}
+	}()
+
 	return s, nil
 }
 
+func (s *Daemon) updateStats(ctx context.Context) {
+	log := s.log
+
+	totalPeers := 0
+	activePeers := 0
+	connectedSeeders := 0
+
+	for _, v := range s.client.Torrents() {
+		stats := v.Stats()
+		err := s.statsStore.AddTorrentStats(v.InfoHash(), TorrentStats{
+			Timestamp:        time.Now(),
+			DownloadedBytes:  uint64(stats.BytesRead.Int64()),
+			UploadedBytes:    uint64(stats.BytesWritten.Int64()),
+			TotalPeers:       uint16(stats.TotalPeers),
+			ActivePeers:      uint16(stats.ActivePeers),
+			ConnectedSeeders: uint16(stats.ConnectedSeeders),
+		})
+		if err != nil {
+			log.Error(ctx, "error saving torrent stats", rlog.Error(err))
+		}
+
+		totalPeers += stats.TotalPeers
+		activePeers += stats.ActivePeers
+		connectedSeeders += stats.ConnectedSeeders
+	}
+
+	totalStats := s.client.Stats()
+	err := s.statsStore.AddTotalStats(TorrentStats{
+		Timestamp:        time.Now(),
+		DownloadedBytes:  uint64(totalStats.BytesRead.Int64()),
+		UploadedBytes:    uint64(totalStats.BytesWritten.Int64()),
+		TotalPeers:       uint16(totalPeers),
+		ActivePeers:      uint16(activePeers),
+		ConnectedSeeders: uint16(connectedSeeders),
+	})
+	if err != nil {
+		log.Error(ctx, "error saving total stats", rlog.Error(err))
+	}
+}
+
+func (s *Daemon) TotalStatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) {
+	return s.statsStore.ReadTotalStatsHistory(ctx, since)
+}
+
+func (s *Daemon) TorrentStatsHistory(ctx context.Context, since time.Time, ih infohash.T) ([]TorrentStats, error) {
+	return s.statsStore.ReadTorrentStatsHistory(ctx, since, ih)
+}
+
+func (s *Daemon) StatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) {
+	return s.statsStore.ReadStatsHistory(ctx, since)
+}
+
 var _ vfs.FsFactory = (*Daemon)(nil).NewTorrentFs
 
 func (s *Daemon) Close(ctx context.Context) error {
@@ -237,7 +312,7 @@ func isValidInfoHashBytes(d []byte) bool {
 }
 
 func (s *Daemon) Stats() torrent.ConnStats {
-	return s.client.ConnStats()
+	return s.client.Stats().ConnStats
 }
 
 const loadWorkers = 5
diff --git a/src/sources/torrent/dht_fileitem_store.go b/src/sources/torrent/dht_fileitem_store.go
index d8cfde5..ae7ea1e 100644
--- a/src/sources/torrent/dht_fileitem_store.go
+++ b/src/sources/torrent/dht_fileitem_store.go
@@ -85,7 +85,25 @@ func (fis *dhtFileItemStore) Get(t bep44.Target) (*bep44.Item, error) {
 }
 
 func (fis *dhtFileItemStore) Del(t bep44.Target) error {
-	// ignore this
+	tx := fis.db.NewTransaction(true)
+	defer tx.Discard()
+
+	err := tx.Delete(t[:])
+	if err == badger.ErrKeyNotFound {
+		return nil
+	}
+	if err != nil {
+		return err
+	}
+
+	err = tx.Commit()
+	if err == badger.ErrKeyNotFound {
+		return nil
+	}
+	if err != nil {
+		return err
+	}
+
 	return nil
 }
 
diff --git a/src/sources/torrent/fs.go b/src/sources/torrent/fs.go
index bbdb74d..fd06cbf 100644
--- a/src/sources/torrent/fs.go
+++ b/src/sources/torrent/fs.go
@@ -155,7 +155,6 @@ func (fs *TorrentFS) files(ctx context.Context) (map[string]vfs.File, error) {
 
 			return fs.filesCache, nil
 		}
-
 	}
 
 DEFAULT_DIR:
diff --git a/src/sources/torrent/stats.go b/src/sources/torrent/stats.go
index d03e961..5444a9a 100644
--- a/src/sources/torrent/stats.go
+++ b/src/sources/torrent/stats.go
@@ -1,218 +1,207 @@
 package torrent
 
 import (
-	"errors"
-	"sync"
+	"context"
+	"encoding/json"
+	"path"
+	"slices"
 	"time"
 
-	"github.com/anacrolix/torrent"
+	"git.kmsign.ru/royalcat/tstor/src/log"
+	"github.com/anacrolix/torrent/types/infohash"
+	"github.com/dgraph-io/badger/v4"
 )
 
-var ErrTorrentNotFound = errors.New("torrent not found")
+func newStatsStore(metaDir string, lifetime time.Duration) (*statsStore, error) {
+	db, err := badger.OpenManaged(
+		badger.
+			DefaultOptions(path.Join(metaDir, "stats")).
+			WithNumVersionsToKeep(int(^uint(0) >> 1)).
+			WithLogger(log.BadgerLogger("stats")), // Infinity
+	)
+	if err != nil {
+		return nil, err
+	}
 
-type PieceStatus string
+	go func() {
+		for n := range time.NewTimer(lifetime / 2).C {
+			db.SetDiscardTs(uint64(n.Add(-lifetime).Unix()))
+		}
+	}()
+	return &statsStore{
+		db: db,
+	}, nil
+}
 
-const (
-	Checking PieceStatus = "H"
-	Partial  PieceStatus = "P"
-	Complete PieceStatus = "C"
-	Waiting  PieceStatus = "W"
-	Error    PieceStatus = "?"
-)
-
-type PieceChunk struct {
-	Status    PieceStatus `json:"status"`
-	NumPieces int         `json:"numPieces"`
+type statsStore struct {
+	db *badger.DB
 }
 
 type TorrentStats struct {
-	Name            string        `json:"name"`
-	Hash            string        `json:"hash"`
-	DownloadedBytes int64         `json:"downloadedBytes"`
-	UploadedBytes   int64         `json:"uploadedBytes"`
-	Peers           int           `json:"peers"`
-	Seeders         int           `json:"seeders"`
-	TimePassed      float64       `json:"timePassed"`
-	PieceChunks     []*PieceChunk `json:"pieceChunks"`
-	TotalPieces     int           `json:"totalPieces"`
-	PieceSize       int64         `json:"pieceSize"`
+	Timestamp        time.Time
+	DownloadedBytes  uint64
+	UploadedBytes    uint64
+	TotalPeers       uint16
+	ActivePeers      uint16
+	ConnectedSeeders uint16
 }
 
-type byName []*TorrentStats
-
-func (a byName) Len() int           { return len(a) }
-func (a byName) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
-func (a byName) Less(i, j int) bool { return a[i].Name < a[j].Name }
-
-type TotalTorrentStats struct {
-	DownloadedBytes int64   `json:"downloadedBytes"`
-	UploadedBytes   int64   `json:"uploadedBytes"`
-	TimePassed      float64 `json:"timePassed"`
+func (s TorrentStats) Same(o TorrentStats) bool {
+	return s.DownloadedBytes == o.DownloadedBytes &&
+		s.UploadedBytes == o.UploadedBytes &&
+		s.TotalPeers == o.TotalPeers &&
+		s.ActivePeers == o.ActivePeers &&
+		s.ConnectedSeeders == o.ConnectedSeeders
 }
 
-type RouteStats struct {
-	Name         string          `json:"name"`
-	TorrentStats []*TorrentStats `json:"torrentStats"`
-}
+func (r *statsStore) addStats(key []byte, stat TorrentStats) error {
+	ts := uint64(stat.Timestamp.Unix())
 
-type ByName []*RouteStats
+	txn := r.db.NewTransactionAt(ts, true)
+	defer txn.Discard()
 
-func (a ByName) Len() int           { return len(a) }
-func (a ByName) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
-func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name }
-
-type stat struct {
-	totalDownloadBytes int64
-	downloadBytes      int64
-	totalUploadBytes   int64
-	uploadBytes        int64
-	peers              int
-	seeders            int
-	time               time.Time
-}
-
-type Stats struct {
-	mut           sync.Mutex
-	torrents      map[string]*torrent.Torrent
-	previousStats map[string]*stat
-
-	gTime time.Time
-}
-
-func NewStats() *Stats {
-	return &Stats{
-		gTime:    time.Now(),
-		torrents: make(map[string]*torrent.Torrent),
-		// torrentsByRoute: make(map[string]map[string]*torrent.Torrent),
-		// previousStats:   make(map[string]*stat),
-	}
-}
-
-func (s *Stats) Add(path string, t *torrent.Torrent) {
-	s.mut.Lock()
-	defer s.mut.Unlock()
-
-	s.torrents[path] = t
-	s.previousStats[path] = &stat{}
-}
-
-func (s *Stats) Del(path string) {
-	s.mut.Lock()
-	defer s.mut.Unlock()
-	delete(s.torrents, path)
-	delete(s.previousStats, path)
-}
-
-func (s *Stats) Stats(path string) (*TorrentStats, error) {
-	s.mut.Lock()
-	defer s.mut.Unlock()
-
-	t, ok := s.torrents[path]
-	if !(ok) {
-		return nil, ErrTorrentNotFound
+	item, err := txn.Get(key)
+	if err != nil && err != badger.ErrKeyNotFound {
+		return err
 	}
 
-	now := time.Now()
-
-	return s.stats(now, t, true), nil
-}
-
-func (s *Stats) GlobalStats() *TotalTorrentStats {
-	s.mut.Lock()
-	defer s.mut.Unlock()
-
-	now := time.Now()
-
-	var totalDownload int64
-	var totalUpload int64
-	for _, torrent := range s.torrents {
-		tStats := s.stats(now, torrent, false)
-		totalDownload += tStats.DownloadedBytes
-		totalUpload += tStats.UploadedBytes
-	}
-
-	timePassed := now.Sub(s.gTime)
-	s.gTime = now
-
-	return &TotalTorrentStats{
-		DownloadedBytes: totalDownload,
-		UploadedBytes:   totalUpload,
-		TimePassed:      timePassed.Seconds(),
-	}
-}
-
-func (s *Stats) stats(now time.Time, t *torrent.Torrent, chunks bool) *TorrentStats {
-	ts := &TorrentStats{}
-	prev, ok := s.previousStats[t.InfoHash().String()]
-	if !ok {
-		return &TorrentStats{}
-	}
-	if s.returnPreviousMeasurements(now) {
-		ts.DownloadedBytes = prev.downloadBytes
-		ts.UploadedBytes = prev.uploadBytes
-	} else {
-		st := t.Stats()
-		rd := st.BytesReadData.Int64()
-		wd := st.BytesWrittenData.Int64()
-		ist := &stat{
-			downloadBytes:      rd - prev.totalDownloadBytes,
-			uploadBytes:        wd - prev.totalUploadBytes,
-			totalDownloadBytes: rd,
-			totalUploadBytes:   wd,
-			time:               now,
-			peers:              st.TotalPeers,
-			seeders:            st.ConnectedSeeders,
+	if err != badger.ErrKeyNotFound {
+		var prevStats TorrentStats
+		err = item.Value(func(val []byte) error {
+			return json.Unmarshal(val, &prevStats)
+		})
+		if err != nil {
+			return err
 		}
 
-		ts.DownloadedBytes = ist.downloadBytes
-		ts.UploadedBytes = ist.uploadBytes
-		ts.Peers = ist.peers
-		ts.Seeders = ist.seeders
-
-		s.previousStats[t.InfoHash().String()] = ist
+		if prevStats.Same(stat) {
+			return nil
+		}
 	}
 
-	ts.TimePassed = now.Sub(prev.time).Seconds()
-	var totalPieces int
-	if chunks {
-		var pch []*PieceChunk
-		for _, psr := range t.PieceStateRuns() {
-			var s PieceStatus
-			switch {
-			case psr.Checking:
-				s = Checking
-			case psr.Partial:
-				s = Partial
-			case psr.Complete:
-				s = Complete
-			case !psr.Ok:
-				s = Error
-			default:
-				s = Waiting
+	data, err := json.Marshal(stat)
+	if err != nil {
+		return err
+	}
+	err = txn.Set(key, data)
+	if err != nil {
+		return err
+	}
+
+	return txn.CommitAt(ts, nil)
+}
+
+func (r *statsStore) AddTorrentStats(ih infohash.T, stat TorrentStats) error {
+	return r.addStats(ih.Bytes(), stat)
+}
+
+const totalKey = "total"
+
+func (r *statsStore) AddTotalStats(stat TorrentStats) error {
+	return r.addStats([]byte(totalKey), stat)
+}
+
+func (r *statsStore) ReadTotalStatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) {
+	stats := []TorrentStats{}
+
+	err := r.db.View(func(txn *badger.Txn) error {
+		opts := badger.DefaultIteratorOptions
+		opts.AllVersions = true
+		opts.SinceTs = uint64(since.Unix())
+
+		it := txn.NewKeyIterator([]byte(totalKey), opts)
+		defer it.Close()
+		for it.Rewind(); it.Valid(); it.Next() {
+			item := it.Item()
+			var stat TorrentStats
+			err := item.Value(func(v []byte) error {
+				return json.Unmarshal(v, &stat)
+			})
+			if err != nil {
+				return err
 			}
 
-			pch = append(pch, &PieceChunk{
-				Status:    s,
-				NumPieces: psr.Length,
-			})
-			totalPieces += psr.Length
+			stats = append(stats, stat)
 		}
-		ts.PieceChunks = pch
+		return nil
+	})
+	if err != nil {
+		return nil, err
 	}
 
-	ts.Hash = t.InfoHash().String()
-	ts.Name = t.Name()
-	ts.TotalPieces = totalPieces
+	slices.SortFunc(stats, func(a, b TorrentStats) int {
+		return a.Timestamp.Compare(b.Timestamp)
+	})
+	stats = slices.Compact(stats)
+	return stats, nil
+}
 
-	if t.Info() != nil {
-		ts.PieceSize = t.Info().PieceLength
+func (r *statsStore) ReadTorrentStatsHistory(ctx context.Context, since time.Time, ih infohash.T) ([]TorrentStats, error) {
+	stats := []TorrentStats{}
+
+	err := r.db.View(func(txn *badger.Txn) error {
+		opts := badger.DefaultIteratorOptions
+		opts.AllVersions = true
+		opts.SinceTs = uint64(since.Unix())
+
+		it := txn.NewKeyIterator(ih.Bytes(), opts)
+		defer it.Close()
+		for it.Rewind(); it.Valid(); it.Next() {
+			item := it.Item()
+			var stat TorrentStats
+			err := item.Value(func(v []byte) error {
+				return json.Unmarshal(v, &stat)
+			})
+			if err != nil {
+				return err
+			}
+
+			stats = append(stats, stat)
+		}
+		return nil
+	})
+	if err != nil {
+		return nil, err
 	}
 
-	return ts
+	slices.SortFunc(stats, func(a, b TorrentStats) int {
+		return a.Timestamp.Compare(b.Timestamp)
+	})
+	stats = slices.Compact(stats)
+	return stats, nil
 }
 
-const gap time.Duration = 2 * time.Second
+func (r *statsStore) ReadStatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) {
+	stats := []TorrentStats{}
 
-func (s *Stats) returnPreviousMeasurements(now time.Time) bool {
-	return now.Sub(s.gTime) < gap
+	err := r.db.View(func(txn *badger.Txn) error {
+		opts := badger.DefaultIteratorOptions
+		opts.AllVersions = true
+		opts.SinceTs = uint64(since.Unix())
+
+		it := txn.NewIterator(opts)
+		defer it.Close()
+		for it.Rewind(); it.Valid(); it.Next() {
+			item := it.Item()
+			var stat TorrentStats
+			err := item.Value(func(v []byte) error {
+				return json.Unmarshal(v, &stat)
+			})
+			if err != nil {
+				return err
+			}
+
+			stats = append(stats, stat)
+		}
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	slices.SortFunc(stats, func(a, b TorrentStats) int {
+		return a.Timestamp.Compare(b.Timestamp)
+	})
+	stats = slices.Compact(stats)
+	return stats, nil
 }
diff --git a/src/sources/torrent/stats_store.go b/src/sources/torrent/stats_store.go
deleted file mode 100644
index b3f4b3e..0000000
--- a/src/sources/torrent/stats_store.go
+++ /dev/null
@@ -1,127 +0,0 @@
-package torrent
-
-import (
-	"context"
-	"encoding/json"
-	"path"
-	"time"
-
-	"git.kmsign.ru/royalcat/tstor/src/log"
-	"github.com/anacrolix/torrent/types/infohash"
-	"github.com/dgraph-io/badger/v4"
-	"golang.org/x/exp/maps"
-)
-
-func newStatsStore(metaDir string, lifetime time.Duration) (*statsStore, error) {
-	db, err := badger.OpenManaged(
-		badger.
-			DefaultOptions(path.Join(metaDir, "stats-history")).
-			WithNumVersionsToKeep(int(^uint(0) >> 1)).
-			WithLogger(log.BadgerLogger("stats")), // Infinity
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	go func() {
-		for n := range time.NewTimer(lifetime / 2).C {
-			db.SetDiscardTs(uint64(n.Add(-lifetime).Unix()))
-		}
-	}()
-	return &statsStore{
-		db: db,
-	}, nil
-}
-
-type statsStore struct {
-	db *badger.DB
-}
-
-func (r *statsStore) AddStat(ih infohash.T, t time.Time, stat TorrentStats) error {
-	data, err := json.Marshal(stat)
-	if err != nil {
-		return err
-	}
-	ts := uint64(t.Unix())
-
-	txn := r.db.NewTransactionAt(ts, false)
-	defer txn.Discard()
-
-	err = txn.Set(ih.Bytes(), data)
-	if err != nil {
-		return err
-	}
-
-	return txn.CommitAt(ts, nil)
-}
-
-func (r *statsStore) ReadTotalStatsHistory(ctx context.Context, since time.Time) ([]TotalTorrentStats, error) {
-	stats := map[time.Time]TotalTorrentStats{}
-
-	err := r.db.View(func(txn *badger.Txn) error {
-		opts := badger.DefaultIteratorOptions
-		opts.AllVersions = true
-		opts.SinceTs = uint64(since.Unix())
-
-		it := txn.NewIterator(opts)
-		defer it.Close()
-		for it.Rewind(); it.Valid(); it.Next() {
-			item := it.Item()
-			// k := item.Key()
-			var tstat TorrentStats
-			err := item.Value(func(v []byte) error {
-				return json.Unmarshal(v, &tstat)
-			})
-			if err != nil {
-				return err
-			}
-
-			t := time.Unix(int64(item.Version()), 0)
-
-			if stat, ok := stats[t]; !ok {
-				stats[t] = TotalTorrentStats{
-					DownloadedBytes: tstat.DownloadedBytes,
-					UploadedBytes:   stat.DownloadedBytes,
-				}
-			} else {
-				stat.DownloadedBytes += tstat.DownloadedBytes
-				stat.UploadedBytes += tstat.UploadedBytes
-				stats[t] = stat
-			}
-
-		}
-		return nil
-	})
-
-	return maps.Values(stats), err
-}
-
-func (r *statsStore) ReadStatsHistory(ctx context.Context, since time.Time, ih infohash.T) ([]TorrentStats, error) {
-	var stats map[time.Time]TorrentStats
-
-	err := r.db.View(func(txn *badger.Txn) error {
-		opts := badger.DefaultIteratorOptions
-		opts.AllVersions = true
-		opts.SinceTs = uint64(since.Unix())
-
-		it := txn.NewKeyIterator(ih.Bytes(), opts)
-		defer it.Close()
-		for it.Rewind(); it.Valid(); it.Next() {
-			item := it.Item()
-			var tstat TorrentStats
-			err := item.Value(func(v []byte) error {
-				return json.Unmarshal(v, &tstat)
-			})
-			if err != nil {
-				return err
-			}
-
-			t := time.Unix(int64(item.Version()), 0)
-
-			stats[t] = tstat
-		}
-		return nil
-	})
-
-	return maps.Values(stats), err
-}
diff --git a/ui/lib/api/schema.graphql b/ui/lib/api/schema.graphql
index 612f7ec..7992756 100644
--- a/ui/lib/api/schema.graphql
+++ b/ui/lib/api/schema.graphql
@@ -99,6 +99,21 @@ type Torrent {
 	excludedFiles: [TorrentFile!]! @resolver
 	peers: [TorrentPeer!]! @resolver
 }
+type TorrentClientStats {
+	bytesWritten: Int!
+	bytesWrittenData: Int!
+	bytesRead: Int!
+	bytesReadData: Int!
+	bytesReadUsefulData: Int!
+	bytesReadUsefulIntendedData: Int!
+	chunksWritten: Int!
+	chunksRead: Int!
+	chunksReadUseful: Int!
+	chunksReadWasted: Int!
+	metadataChunksRead: Int!
+	piecesDirtiedGood: Int!
+	piecesDirtiedBad: Int!
+}
 type TorrentDaemonMutation {
 	validateTorrent(filter: TorrentFilter!): Boolean! @resolver
 	setTorrentPriority(infohash: String!, file: String, priority: TorrentPriority!): Boolean! @resolver
@@ -106,7 +121,8 @@ type TorrentDaemonMutation {
 }
 type TorrentDaemonQuery {
 	torrents(filter: TorrentsFilter): [Torrent!]! @resolver
-	stats: TorrentStats! @resolver
+	clientStats: TorrentClientStats! @resolver
+	statsHistory(since: DateTime!, infohash: String): [TorrentStats!]! @resolver
 }
 type TorrentFS implements Dir & FsEntry {
 	name: String!
@@ -156,19 +172,12 @@ type TorrentProgress implements Progress {
 	total: Int!
 }
 type TorrentStats {
-	bytesWritten: Int!
-	bytesWrittenData: Int!
-	bytesRead: Int!
-	bytesReadData: Int!
-	bytesReadUsefulData: Int!
-	bytesReadUsefulIntendedData: Int!
-	chunksWritten: Int!
-	chunksRead: Int!
-	chunksReadUseful: Int!
-	chunksReadWasted: Int!
-	metadataChunksRead: Int!
-	piecesDirtiedGood: Int!
-	piecesDirtiedBad: Int!
+	timestamp: DateTime!
+	downloadedBytes: UInt!
+	uploadedBytes: UInt!
+	totalPeers: UInt!
+	activePeers: UInt!
+	connectedSeeders: UInt!
 }
 input TorrentsFilter {
 	infohash: StringFilter
@@ -178,4 +187,5 @@ input TorrentsFilter {
 	peersCount: IntFilter
 	priority: TorrentPriorityFilter
 }
+scalar UInt
 scalar Upload