~evanj/cms

0ac69893cd1dd4e4cf2fa1498b99682d247e30cd — Evan M Jones 4 years ago 9d7fbcb
Feat(invites and roles): Users can now set roles while inviting others.
M internal/c/c.go => internal/c/c.go +2 -0
@@ 11,6 11,7 @@ import (
	"net/http"
	"strings"

	"git.sr.ht/~evanj/cms/internal/m/role"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/s/rl"
	"github.com/google/uuid"


@@ 148,6 149,7 @@ func (c *Controller) HTML(w http.ResponseWriter, r *http.Request, tmpl *template
	}

	data["Build"] = c.buildID
	data["Roles"] = role.Roles

	if err := tmpl.Execute(&buf, data); err != nil {
		c.log.Println(err)

M internal/c/invite/invite.go => internal/c/invite/invite.go +10 -3
@@ 10,6 10,7 @@ import (
	"git.sr.ht/~evanj/cms/internal/c"
	"git.sr.ht/~evanj/cms/internal/m/invite"
	"git.sr.ht/~evanj/cms/internal/m/org"
	"git.sr.ht/~evanj/cms/internal/m/role"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/v"
)


@@ 28,7 29,7 @@ type Invite struct {
}

type dber interface {
	InviteNew(u user.User, o org.Org) (invite.Invite, error)
	InviteNew(u user.User, o org.Org, r role.Role) (invite.Invite, error)
	InviteGetByToken(tok string) (invite.Invite, error)
	InviteAccept(i invite.Invite, u, p, v string) (user.User, invite.Invite, error)
	InviteList(u user.User, o org.Org) (r []invite.Invite, err error)


@@ 73,7 74,13 @@ func (i Invite) ServeHTTP(w http.ResponseWriter, r *http.Request) {
			return
		}

		_, err = i.db.InviteNew(user, user.Org())
		role, ok := role.ByName(r.FormValue("role"))
		if !ok {
			i.Error2(w, r, http.StatusBadRequest, errors.New("invalid role suppplied for invite"))
			return
		}

		_, err = i.db.InviteNew(user, user.Org(), role)
		if errors.Is(err, invite.ErrExpired) || errors.Is(err, invite.ErrUsed) {
			i.Error2(w, r, http.StatusBadRequest, err)
			return


@@ 95,7 102,7 @@ func (i Invite) ServeHTTP(w http.ResponseWriter, r *http.Request) {
		}

		inv, err := i.db.InviteGetByToken(r.FormValue("invite"))
		if err == nil {
		if err != nil {
			i.Error2(w, r, http.StatusBadRequest, err)
			return
		}

M internal/m/invite/invite.go => internal/m/invite/invite.go +2 -0
@@ 4,6 4,7 @@ import (
	"errors"

	"git.sr.ht/~evanj/cms/internal/m/org"
	"git.sr.ht/~evanj/cms/internal/m/role"
)

var (


@@ 16,4 17,5 @@ type Invite interface {
	Token() string
	Validate() error
	Org() org.Org
	Role() role.Role
}

M internal/m/role/role.go => internal/m/role/role.go +11 -0
@@ 145,4 145,15 @@ var (
		HookGet:        true,
		ContentGet:     true,
	}}

	Roles = []Role{Admin, Developer, Editor, Author, Contributor, Reader}
)

func ByName(n string) (Role, bool) {
	for _, t := range Roles {
		if t.Name == n {
			return t, true
		}
	}
	return Reader, false
}

M internal/s/db/db.go => internal/s/db/db.go +10 -1
@@ 1,12 1,15 @@
package db

import (
	"bytes"
	"database/sql"
	"html/template"
	"log"
	"sort"
	"strconv"
	"strings"

	"git.sr.ht/~evanj/cms/internal/m/role"
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
	"git.sr.ht/~evanj/security"
	_ "github.com/go-sql-driver/mysql"


@@ 19,7 22,13 @@ func newMigrationSlice(m map[string]string) [][]string {
	i := 0

	for key, val := range m {
		migrations[i] = []string{key, val}
		buf := &bytes.Buffer{}
		// Yes. Fail on startup if we can't process migrations.
		template.Must(template.New(key).Parse(val)).Execute(buf, map[string]interface{}{
			"Roles": role.Roles,
		})

		migrations[i] = []string{key, buf.String()}
		i++
	}


M internal/s/db/invite.go => internal/s/db/invite.go +19 -11
@@ 8,6 8,7 @@ import (

	"git.sr.ht/~evanj/cms/internal/m/invite"
	"git.sr.ht/~evanj/cms/internal/m/org"
	"git.sr.ht/~evanj/cms/internal/m/role"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/security"
)


@@ 17,6 18,7 @@ type Invite struct {
	id, createdStr, expiresStr string
	used                       bool
	InviteOrg                  Org
	InviteRole                 Role
	// Set on get.
	created, expires time.Time
	tok              string


@@ 24,19 26,20 @@ type Invite struct {

var (
	queryCreate = `
		INSERT INTO cms_invite (EXPIRES, ORG_ID, USED) VALUES (?, ?, FALSE)
		INSERT INTO cms_invite (EXPIRES, ORG_ID, USED, ROLE_ID) VALUES (?, ?, FALSE, (SELECT cms_role.ID FROM cms_role WHERE VALUE=?))
	`

	queryGet = `
		SELECT cms_invite.ID, CREATED, EXPIRES, USED, cms_org.ID, cms_billing.TIER_NAME, cms_billing.PAYMENT_CUSTOMER 
		SELECT cms_invite.ID, CREATED, EXPIRES, USED, cms_org.ID, cms_billing.TIER_NAME, cms_billing.PAYMENT_CUSTOMER, cms_role.ID, cms_role.VALUE
		FROM cms_invite 
		JOIN cms_org ON cms_org.ID=cms_invite.ORG_ID
		JOIN cms_role ON cms_role.ID=cms_invite.ROLE_ID
		LEFT JOIN cms_billing ON cms_billing.ORG_ID=cms_org.ID
		WHERE cms_invite.ID=?
	`

	queryList = `
		SELECT ID FROM cms_invite WHERE ORG_ID=? AND EXPIRES < ? AND EXPIRES > ? AND USED=FALSE ORDER BY ID DESC
		SELECT ID FROM cms_invite WHERE ORG_ID=? AND EXPIRES > ? AND USED=FALSE ORDER BY ID DESC
	`

	queryUse = `


@@ 46,14 49,14 @@ var (
	mysqlTimeLayout = "2006-01-02 15:04:05"
)

func (db *DB) InviteNew(u user.User, o org.Org) (invite.Invite, error) {
func (db *DB) InviteNew(u user.User, o org.Org, r role.Role) (invite.Invite, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	i, err := db.inviteNew(t, o)
	i, err := db.inviteNew(t, o, r)
	if err != nil {
		return nil, err
	}


@@ 61,8 64,8 @@ func (db *DB) InviteNew(u user.User, o org.Org) (invite.Invite, error) {
	return i, t.Commit()
}

func (db *DB) inviteNew(t *sql.Tx, o org.Org) (invite.Invite, error) {
	res, err := t.Exec(queryCreate, time.Now().UTC().Add(1*time.Hour), o.ID())
func (db *DB) inviteNew(t *sql.Tx, o org.Org, r role.Role) (invite.Invite, error) {
	res, err := t.Exec(queryCreate, time.Now().UTC().Add(1*time.Hour), o.ID(), r.Name)
	if err != nil {
		return nil, err
	}


@@ 95,6 98,7 @@ func (db *DB) inviteGet(t *sql.Tx, id string) (invite.Invite, error) {
	if err := t.QueryRow(queryGet, id).Scan(
		&i.id, &i.createdStr, &i.expiresStr, &i.used,
		&i.InviteOrg.OrgID, &i.InviteOrg.OrgBillingTierName, &i.InviteOrg.OrgPaymentCustomer,
		&i.InviteRole.RoleID, &i.InviteRole.RoleName,
	); err != nil {
		return nil, err
	}


@@ 147,7 151,7 @@ func (db *DB) inviteAccept(t *sql.Tx, i invite.Invite, u, p, v string) (user.Use
		return nil, nil, err
	}

	if _, err := t.Exec(queryCreateNewUser, u, hash, i.Org().ID()); err != nil {
	if _, err := t.Exec(queryCreateNewUser, u, hash, i.Org().ID(), i.Role().Name); err != nil {
		return nil, nil, fmt.Errorf("user '%s' already exists", u)
	}



@@ 163,11 167,9 @@ func (db *DB) InviteList(u user.User, o org.Org) (r []invite.Invite, err error) 
	var (
		now  = time.Now().UTC()
		from = now.Format(mysqlTimeLayout)
		// Add a second here. Offset it slightly so user receives expected results.
		to = now.Add(1 * time.Hour).Add(1 * time.Second).Format(mysqlTimeLayout)
	)

	rows, err := db.Query(queryList, o.ID(), to, from)
	rows, err := db.Query(queryList, o.ID(), from)
	if err != nil {
		return nil, err
	}


@@ 236,6 238,12 @@ func (i Invite) ID() string    { return i.id }
func (i Invite) Token() string { return i.tok }
func (i Invite) Org() org.Org  { return i.InviteOrg }

func (i Invite) Role() role.Role {
	// NOTE: Guaranteed to have this set appropriately.
	r, _ := role.ByName(i.InviteRole.RoleName)
	return r
}

func (i Invite) Validate() error {
	if i.used {
		return invite.ErrUsed

M internal/s/db/migrations_embed.go => internal/s/db/migrations_embed.go +2 -0
@@ 28,6 28,8 @@ func init() {

	migrations["sql/00007.sql"] = tostring("Q1JFQVRFIFRBQkxFIGNtc19pbnZpdGUgKCAKCUlEIElOVEVHRVIgUFJJTUFSWSBLRVkgQVVUT19JTkNSRU1FTlQsCiAgQ1JFQVRFRCBUSU1FU1RBTVAgTk9UIE5VTEwgREVGQVVMVCBDVVJSRU5UX1RJTUVTVEFNUCwKICBFWFBJUkVTIFRJTUVTVEFNUCBOT1QgTlVMTCwKICBVU0VEIEJPT0xFQU4sCiAgT1JHX0lEIElOVEVHRVIgTk9UIE5VTEwsCiAgQ09OU1RSQUlOVCBDTVNfSU5WSVRFX1RPX09SR19GSyBGT1JFSUdOIEtFWShPUkdfSUQpIFJFRkVSRU5DRVMgY21zX29yZyhJRCkKKTsK")

	migrations["sql/00008.sql"] = tostring("REVMRVRFIEZST00gY21zX2ludml0ZTsKCkNSRUFURSBUQUJMRSBjbXNfcm9sZSAoCglJRCBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9fSU5DUkVNRU5ULAoJVkFMVUUgdmFyY2hhcigyNTYpIFVOSVFVRSBOT1QgTlVMTAopOwoKe3tyYW5nZSAuUm9sZXN9fQpJTlNFUlQgSU5UTyBjbXNfcm9sZSAoVkFMVUUpIHZhbHVlcyAoInt7Lk5hbWV9fSIpOwp7e2VuZH19CgpBTFRFUiBUQUJMRSBjbXNfaW52aXRlIEFERCBST0xFX0lEIElOVEVHRVIgTk9UIE5VTEw7CkFMVEVSIFRBQkxFIGNtc19pbnZpdGUgQUREIENPTlNUUkFJTlQgQ01TX0lOVklURV9ST0xFX0lEX0ZLIEZPUkVJR04gS0VZKFJPTEVfSUQpIFJFRkVSRU5DRVMgY21zX3JvbGUoSUQpOwoKQUxURVIgVEFCTEUgY21zX3VzZXIgQUREIFJPTEVfSUQgSU5URUdFUiBOT1QgTlVMTDsKVVBEQVRFIGNtc191c2VyIFNFVCBST0xFX0lEPShTRUxFQ1QgSUQgRlJPTSBjbXNfcm9sZSBXSEVSRSBWQUxVRT0iQWRtaW4iKTsKQUxURVIgVEFCTEUgY21zX3VzZXIgQUREIENPTlNUUkFJTlQgQ01TX1VTRVJfUk9MRV9JRF9GSyBGT1JFSUdOIEtFWShST0xFX0lEKSBSRUZFUkVOQ0VTIGNtc19yb2xlKElEKTsK")

}

func Get(name string) (string, bool) {

A internal/s/db/role.go => internal/s/db/role.go +6 -0
@@ 0,0 1,6 @@
package db

type Role struct {
	RoleID   string
	RoleName string
}

A internal/s/db/sql/00008.sql => internal/s/db/sql/00008.sql +17 -0
@@ 0,0 1,17 @@
DELETE FROM cms_invite;

CREATE TABLE cms_role (
	ID INTEGER PRIMARY KEY AUTO_INCREMENT,
	VALUE varchar(256) UNIQUE NOT NULL
);

{{range .Roles}}
INSERT INTO cms_role (VALUE) values ("{{.Name}}");
{{end}}

ALTER TABLE cms_invite ADD ROLE_ID INTEGER NOT NULL;
ALTER TABLE cms_invite ADD CONSTRAINT CMS_INVITE_ROLE_ID_FK FOREIGN KEY(ROLE_ID) REFERENCES cms_role(ID);

ALTER TABLE cms_user ADD ROLE_ID INTEGER NOT NULL;
UPDATE cms_user SET ROLE_ID=(SELECT ID FROM cms_role WHERE VALUE="Admin");
ALTER TABLE cms_user ADD CONSTRAINT CMS_USER_ROLE_ID_FK FOREIGN KEY(ROLE_ID) REFERENCES cms_role(ID);

M internal/s/db/user.go => internal/s/db/user.go +21 -11
@@ 15,6 15,7 @@ type User struct {
	UserID    string
	UserName  string
	UserOrg   Org
	UserRole  Role
	userHash  string
	userEmail sql.NullString
	// Set on read.


@@ 24,21 25,23 @@ type User struct {
// SQL QUERIES

var (
	queryCreateNewUser = `INSERT INTO cms_user (NAME, HASH, ORG_ID) VALUES (?, ?, ?)`
	queryCreateNewUser = `INSERT INTO cms_user (NAME, HASH, ORG_ID, ROLE_ID) VALUES (?, ?, ?, (SELECT cms_role.ID FROM cms_role WHERE VALUE=?))`

	queryFindUserByID = `
		SELECT cms_user.ID, NAME, HASH, EMAIL, cms_org.ID, cms_billing.TIER_NAME, cms_billing.PAYMENT_CUSTOMER 
		SELECT cms_user.ID, NAME, HASH, EMAIL, cms_org.ID, cms_billing.TIER_NAME, cms_billing.PAYMENT_CUSTOMER, cms_role.ID, cms_role.VALUE
		FROM cms_user 
		JOIN cms_org ON cms_org.ID=cms_user.ORG_ID
		join cms_role ON cms_role.ID=cms_user.ROLE_ID
		LEFT JOIN cms_billing ON cms_billing.ORG_ID=cms_org.ID
		LEFT JOIN cms_email ON cms_email.USER_ID=cms_user.ID 
		WHERE cms_user.ID = ?
	`

	queryFindUserByName = `
		SELECT cms_user.ID, NAME, HASH, EMAIL, cms_org.ID, cms_billing.TIER_NAME, cms_billing.PAYMENT_CUSTOMER 
		SELECT cms_user.ID, NAME, HASH, EMAIL, cms_org.ID, cms_billing.TIER_NAME, cms_billing.PAYMENT_CUSTOMER, cms_role.ID, cms_role.VALUE
		FROM cms_user 
		JOIN cms_org ON cms_org.ID=cms_user.ORG_ID
		join cms_role ON cms_role.ID=cms_user.ROLE_ID
		LEFT JOIN cms_billing ON cms_billing.ORG_ID=cms_org.ID
		LEFT JOIN cms_email ON cms_email.USER_ID=cms_user.ID 
		WHERE NAME = ?


@@ 88,7 91,7 @@ func (db *DB) userNew(t *sql.Tx, username, password, verifyPassword string) (use
		return nil, err
	}

	if _, err := t.Exec(queryCreateNewUser, username, hash, org.ID()); err != nil {
	if _, err := t.Exec(queryCreateNewUser, username, hash, org.ID(), role.Admin.Name); err != nil {
		return nil, fmt.Errorf("user '%s' already exists", username)
	}



@@ 115,6 118,7 @@ func (db *DB) userGet(t *sql.Tx, username, password string) (user.User, error) {
	if err := t.QueryRow(queryFindUserByName, username).Scan(
		&user.UserID, &user.UserName, &user.userHash, &user.userEmail,
		&user.UserOrg.OrgID, &user.UserOrg.OrgBillingTierName, &user.UserOrg.OrgPaymentCustomer,
		&user.UserRole.RoleID, &user.UserRole.RoleName,
	); err != nil {
		return nil, fmt.Errorf("failed to find user '%s'", username)
	}


@@ 147,6 151,7 @@ func (db *DB) UserGetFromToken(token string) (user.User, error) {
	if err := db.QueryRow(queryFindUserByID, id).Scan(
		&user.UserID, &user.UserName, &user.userHash, &user.userEmail,
		&user.UserOrg.OrgID, &user.UserOrg.OrgBillingTierName, &user.UserOrg.OrgPaymentCustomer,
		&user.UserRole.RoleID, &user.UserRole.RoleName,
	); err != nil {

		fmt.Println(err)


@@ 202,10 207,15 @@ func (db *DB) UserSetPassword(u user.User, current, password, verifyPassword str
	return db.UserGet(u.Name(), password)
}

func (u *User) ID() string      { return u.UserID }
func (u *User) Name() string    { return u.UserName }
func (u *User) Token() string   { return u.userToken }
func (u *User) Org() org.Org    { return u.UserOrg }
func (u *User) HasEmail() bool  { return u.userEmail.Valid }
func (u *User) Email() string   { return u.userEmail.String }
func (u *User) Role() role.Role { return role.Admin } // TODO: Actual impl.
func (u *User) ID() string     { return u.UserID }
func (u *User) Name() string   { return u.UserName }
func (u *User) Token() string  { return u.userToken }
func (u *User) Org() org.Org   { return u.UserOrg }
func (u *User) HasEmail() bool { return u.userEmail.Valid }
func (u *User) Email() string  { return u.userEmail.String }

func (u *User) Role() role.Role {
	// NOTE: Guaranteed to have this set appropriately.
	r, _ := role.ByName(u.UserRole.RoleName)
	return r
}

M internal/s/rbac/rbac.go => internal/s/rbac/rbac.go +2 -2
@@ 74,9 74,9 @@ func (rbac RBAC) SpaceDelete(user user.User, space space.Space) error {

// INVITE

func (rbac RBAC) InviteNew(u user.User, o org.Org) (invite.Invite, error) {
func (rbac RBAC) InviteNew(u user.User, o org.Org, r role.Role) (invite.Invite, error) {
	if u.Role().Can(role.InviteCreate) {
		return rbac.db.InviteNew(u, o)
		return rbac.db.InviteNew(u, o, r)
	}
	return nil, role.ErrNoPermission
}

M internal/s/rl/rl.go => internal/s/rl/rl.go +4 -3
@@ 12,6 12,7 @@ import (
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/invite"
	"git.sr.ht/~evanj/cms/internal/m/org"
	"git.sr.ht/~evanj/cms/internal/m/role"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/tier"
	"git.sr.ht/~evanj/cms/internal/m/user"


@@ 176,11 177,11 @@ func (rl RL) ContentTypeUpdate(u user.User, space space.Space, contenttype conte

// Rate limit users to org.

func (rl RL) InviteNew(u user.User, o org.Org) (invite.Invite, error) {
func (rl RL) InviteNew(u user.User, o org.Org, r role.Role) (invite.Invite, error) {
	limit, ok := userLimits[o.Tier().Name]
	if !ok {
		// If not in map, unlimited.
		return rl.db.InviteNew(u, o)
		return rl.db.InviteNew(u, o, r)
	}

	c, err := rl.db.OrgGetUserCount(o)


@@ 192,7 193,7 @@ func (rl RL) InviteNew(u user.User, o org.Org) (invite.Invite, error) {
		return nil, fmt.Errorf("can't invite new users: %w", ErrHitLimit)
	}

	return rl.db.InviteNew(u, o)
	return rl.db.InviteNew(u, o, r)
}

func (rl RL) InviteAccept(i invite.Invite, u, p, v string) (user.User, invite.Invite, error) {

M internal/v/html/_footer.html => internal/v/html/_footer.html +11 -0
@@ 91,6 91,17 @@
          <p>We'll generate a special link for you. Send this to your friend, 
          coworker, or whoever!</p>
          <p>The invite will only be active for one hour.</p>
          <div class='form-group row'>
            <div class='col-12'>
              <label for=role>User's desired role</label>
              <select id=role class="w-100 form-control" name=role required>
                <option disabled value>Role</option>
                {{range .Roles}}
                <option value="{{.Name}}">{{.Name}}</option>
                {{end}}
              </select>
            </div>
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>

M internal/v/tmpls_embed.go => internal/v/tmpls_embed.go +1 -1
@@ 20,7 20,7 @@ func init() {

	tmpls["css/mvp.css"] = tostring("OnJvb3QgewogICAgLS1ib3JkZXItcmFkaXVzOiA1cHg7CiAgICAtLWJveC1zaGFkb3c6IDJweCAycHggMTBweDsKICAgIC0tY29sb3I6ICMxMThiZWU7CiAgICAtLWNvbG9yLWFjY2VudDogIzExOGJlZTBiOwogICAgLS1jb2xvci1iZzogI2ZmZjsKICAgIC0tY29sb3ItYmctc2Vjb25kYXJ5OiAjZTllOWU5OwogICAgLS1jb2xvci1zZWNvbmRhcnk6ICM5MjBkZTk7CiAgICAtLWNvbG9yLXNlY29uZGFyeS1hY2NlbnQ6ICM5MjBkZTkwYjsKICAgIC0tY29sb3Itc2hhZG93OiAjZjRmNGY0OwogICAgLS1jb2xvci10ZXh0OiAjMDAwOwogICAgLS1jb2xvci10ZXh0LXNlY29uZGFyeTogIzk5OTsKICAgIC0taG92ZXItYnJpZ2h0bmVzczogMS4yOwogICAgLS1qdXN0aWZ5LWltcG9ydGFudDogY2VudGVyOwogICAgLS1qdXN0aWZ5LW5vcm1hbDogbGVmdDsKICAgIC0tbGluZS1oZWlnaHQ6IDE1MCU7CiAgICAtLXdpZHRoLWNhcmQ6IDI4NXB4OwogICAgLS13aWR0aC1jYXJkLW1lZGl1bTogNDYwcHg7CiAgICAtLXdpZHRoLWNhcmQtd2lkZTogODAwcHg7CiAgICAtLXdpZHRoLWNvbnRlbnQ6IDEwODBweDsKfQoKLyogTVZQLmNzcyB2MS4wIC0gYnkgQW5keSBCcmV3ZXIgKi8KCi8qIExheW91dCAqLwphcnRpY2xlIGFzaWRlIHsKICAgIGJhY2tncm91bmQ6IHZhcigtLWNvbG9yLXNlY29uZGFyeS1hY2NlbnQpOwogICAgYm9yZGVyLWxlZnQ6IDRweCBzb2xpZCB2YXIoLS1jb2xvci1zZWNvbmRhcnkpOwogICAgcGFkZGluZzogMC4wMXJlbSAwLjhyZW07Cn0KCmJvZHkgewogICAgYmFja2dyb3VuZDogdmFyKC0tY29sb3ItYmcpOwogICAgY29sb3I6IHZhcigtLWNvbG9yLXRleHQpOwogICAgZm9udC1mYW1pbHk6IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgIlNlZ29lIFVJIiwgUm9ib3RvLCBPeHlnZW4tU2FucywgVWJ1bnR1LCBDYW50YXJlbGwsICJIZWx2ZXRpY2EgTmV1ZSIsIHNhbnMtc2VyaWY7CiAgICBsaW5lLWhlaWdodDogdmFyKC0tbGluZS1oZWlnaHQpOwogICAgbWFyZ2luOiAwOwogICAgb3ZlcmZsb3cteDogaGlkZGVuOwogICAgcGFkZGluZzogMXJlbSAwOwp9Cgpmb290ZXIsCmhlYWRlciwKbWFpbiB7CiAgICBtYXJnaW46IDAgYXV0bzsKICAgIG1heC13aWR0aDogdmFyKC0td2lkdGgtY29udGVudCk7CiAgICBwYWRkaW5nOiAycmVtIDFyZW07Cn0KCmhyIHsKICAgIGJhY2tncm91bmQtY29sb3I6IHZhcigtLWNvbG9yLWJnLXNlY29uZGFyeSk7CiAgICBib3JkZXI6IG5vbmU7CiAgICBoZWlnaHQ6IDFweDsKICAgIG1hcmdpbjogNHJlbSAwOwp9CgpzZWN0aW9uIHsKICAgIGRpc3BsYXk6IGZsZXg7CiAgICBmbGV4LXdyYXA6IHdyYXA7CiAgICBqdXN0aWZ5LWNvbnRlbnQ6IHZhcigtLWp1c3RpZnktaW1wb3J0YW50KTsKfQoKc2VjdGlvbiBhc2lkZSB7CiAgICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1jb2xvci1iZy1zZWNvbmRhcnkpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBib3gtc2hhZG93OiB2YXIoLS1ib3gtc2hhZG93KSB2YXIoLS1jb2xvci1zaGFkb3cpOwogICAgbWFyZ2luOiAxcmVtOwogICAgcGFkZGluZzogMS4yNXJlbTsKICAgIHdpZHRoOiB2YXIoLS13aWR0aC1jYXJkKTsKfQoKc2VjdGlvbiBhc2lkZTpob3ZlciB7CiAgICBib3gtc2hhZG93OiB2YXIoLS1ib3gtc2hhZG93KSB2YXIoLS1jb2xvci1iZy1zZWNvbmRhcnkpOwp9CgpzZWN0aW9uIGFzaWRlIGltZyB7CiAgICBtYXgtd2lkdGg6IDEwMCU7Cn0KCi8qIEhlYWRlcnMgKi8KYXJ0aWNsZSBoZWFkZXIsCmRpdiBoZWFkZXIsCm1haW4gaGVhZGVyIHsKICAgIHBhZGRpbmctdG9wOiAwOwp9CgpoZWFkZXIgewogICAgdGV4dC1hbGlnbjogdmFyKC0tanVzdGlmeS1pbXBvcnRhbnQpOwp9CgpoZWFkZXIgYSBiLApoZWFkZXIgYSBlbSwKaGVhZGVyIGEgaSwKaGVhZGVyIGEgc3Ryb25nIHsKICAgIG1hcmdpbi1sZWZ0OiAxcmVtOwogICAgbWFyZ2luLXJpZ2h0OiAxcmVtOwp9CgpoZWFkZXIgbmF2IGltZyB7CiAgICBtYXJnaW46IDFyZW0gMDsKfQoKc2VjdGlvbiBoZWFkZXIgewogICAgcGFkZGluZy10b3A6IDA7CiAgICB3aWR0aDogMTAwJTsKfQoKLyogTmF2ICovCm5hdiB7CiAgICBhbGlnbi1pdGVtczogY2VudGVyOwogICAgZGlzcGxheTogZmxleDsKICAgIGZvbnQtd2VpZ2h0OiBib2xkOwogICAganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuOwogICAgbWFyZ2luLWJvdHRvbTogN3JlbTsKfQoKbmF2IHVsIHsKICAgIGxpc3Qtc3R5bGU6IG5vbmU7CiAgICBwYWRkaW5nOiAwOwp9CgpuYXYgdWwgbGkgewogICAgZGlzcGxheTogaW5saW5lLWJsb2NrOwogICAgbWFyZ2luOiAwIDAuNXJlbTsKfQoKLyogVHlwb2dyYXBoeSAqLwpjb2RlIHsKICAgIGRpc3BsYXk6IGlubGluZS1ibG9jazsKICAgIG1hcmdpbjogMCAwLjFyZW07CiAgICBwYWRkaW5nOiAwcmVtIDAuNXJlbTsKfQoKY29kZSwKc2FtcCB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvci1hY2NlbnQpOwogICAgY29sb3I6IHZhcigtLWNvbG9yLXRleHQpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICB0ZXh0LWFsaWduOiB2YXIoLS1qdXN0aWZ5LW5vcm1hbCk7Cn0KCmgxLApoMiwKaDMsCmg0LApoNSwKaDYgewogICAgbGluZS1oZWlnaHQ6IHZhcigtLWxpbmUtaGVpZ2h0KTsKfQoKbWFyayB7CiAgICBwYWRkaW5nOiAwLjFyZW07Cn0KCm9sIGxpLAp1bCBsaSB7CiAgICBwYWRkaW5nOiAwLjJyZW0gMDsKfQoKcCB7CiAgICBtYXJnaW46IDAuNzVyZW0gMDsKICAgIHBhZGRpbmc6IDA7Cn0KCnNhbXAgewogICAgZGlzcGxheTogYmxvY2s7CiAgICBtYXJnaW46IDFyZW0gMDsKICAgIG1heC13aWR0aDogdmFyKC0td2lkdGgtY2FyZC13aWRlKTsKICAgIHBhZGRpbmc6IDFyZW07Cn0KCnNtYWxsIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci10ZXh0LXNlY29uZGFyeSk7Cn0KCnN1cCB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvci1zZWNvbmRhcnkpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBjb2xvcjogdmFyKC0tY29sb3ItYmcpOwogICAgZm9udC1zaXplOiB4eC1zbWFsbDsKICAgIGZvbnQtd2VpZ2h0OiBib2xkOwogICAgbWFyZ2luOiAwLjJyZW07CiAgICBwYWRkaW5nOiAwLjJyZW0gMC4zcmVtOwogICAgcG9zaXRpb246IHJlbGF0aXZlOwogICAgdG9wOiAtMnB4Owp9CgovKiBMaW5rcyAqLwphIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci1zZWNvbmRhcnkpOwogICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7Cn0KCmE6aG92ZXIgewogICAgZmlsdGVyOiBicmlnaHRuZXNzKHZhcigtLWhvdmVyLWJyaWdodG5lc3MpKTsKICAgIHRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lOwp9CgphIGIsCmEgZW0sCmEgaSwKYSBzdHJvbmcsCmJ1dHRvbiB7CiAgICBib3JkZXItcmFkaXVzOiB2YXIoLS1ib3JkZXItcmFkaXVzKTsKICAgIGRpc3BsYXk6IGlubGluZS1ibG9jazsKICAgIGZvbnQtc2l6ZTogbWVkaXVtOwogICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICBtYXJnaW46IDEuNXJlbSAwIDAuNXJlbSAwOwogICAgcGFkZGluZzogMXJlbSAycmVtOwp9CgppbnB1dFt0eXBlPXN1Ym1pdF06aG92ZXIsCmJ1dHRvbjpob3ZlciB7CiAgICBjdXJzb3I6IHBvaW50ZXI7CiAgICBmaWx0ZXI6IGJyaWdodG5lc3ModmFyKC0taG92ZXItYnJpZ2h0bmVzcykpOwp9CgphIGIsCmEgc3Ryb25nLAppbnB1dFt0eXBlPXN1Ym1pdF0sCmJ1dHRvbiB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvcik7CiAgICBib3JkZXI6IDJweCBzb2xpZCB2YXIoLS1jb2xvcik7CiAgICBjb2xvcjogdmFyKC0tY29sb3ItYmcpOwp9CgphIGVtLAphIGkgewogICAgYm9yZGVyOiAycHggc29saWQgdmFyKC0tY29sb3IpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBjb2xvcjogdmFyKC0tY29sb3IpOwogICAgZGlzcGxheTogaW5saW5lLWJsb2NrOwogICAgcGFkZGluZzogMXJlbSAycmVtOwp9CgovKiBJbWFnZXMgKi8KZmlndXJlIHsKICAgIG1hcmdpbjogMDsKICAgIHBhZGRpbmc6IDA7Cn0KCmZpZ3VyZSBmaWdjYXB0aW9uIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci10ZXh0LXNlY29uZGFyeSk7Cn0KCi8qIEZvcm1zICovCmZvcm0gewogICAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tY29sb3ItYmctc2Vjb25kYXJ5KTsKICAgIGJvcmRlci1yYWRpdXM6IHZhcigtLWJvcmRlci1yYWRpdXMpOwogICAgYm94LXNoYWRvdzogdmFyKC0tYm94LXNoYWRvdykgdmFyKC0tY29sb3Itc2hhZG93KTsKICAgIGRpc3BsYXk6IGJsb2NrOwogICAgbWF4LXdpZHRoOiB2YXIoLS13aWR0aC1jYXJkLXdpZGUpOwogICAgbWluLXdpZHRoOiB2YXIoLS13aWR0aC1jYXJkKTsKICAgIHBhZGRpbmc6IDEuNXJlbTsKICAgIHRleHQtYWxpZ246IHZhcigtLWp1c3RpZnktbm9ybWFsKTsKfQoKZm9ybSBoZWFkZXIgewogICAgbWFyZ2luOiAxLjVyZW0gMDsKICAgIHBhZGRpbmc6IDEuNXJlbSAwOwp9CgppbnB1dCwKbGFiZWwsCnNlbGVjdCwKdGV4dGFyZWEgewogICAgZGlzcGxheTogYmxvY2s7CiAgICBmb250LXNpemU6IGluaGVyaXQ7CiAgICBtYXgtd2lkdGg6IHZhcigtLXdpZHRoLWNhcmQtd2lkZSk7Cn0KCmlucHV0LApzZWxlY3QsCnRleHRhcmVhIHsKICAgIG1hcmdpbi1ib3R0b206IDFyZW07Cn0KCmlucHV0LApzZWxlY3QsCnRleHRhcmVhIHsKICAgIGJvcmRlcjogMXB4IHNvbGlkIHZhcigtLWNvbG9yLWJnLXNlY29uZGFyeSk7CiAgICBib3JkZXItcmFkaXVzOiB2YXIoLS1ib3JkZXItcmFkaXVzKTsKICAgIHBhZGRpbmc6IDAuNHJlbSAwLjhyZW07Cn0KCmxhYmVsIHsKICAgIGZvbnQtd2VpZ2h0OiBib2xkOwogICAgbWFyZ2luLWJvdHRvbTogMC4ycmVtOwp9CgovKiBUYWJsZXMgKi8KdGFibGUgewogICAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tY29sb3ItYmctc2Vjb25kYXJ5KTsKICAgIGJvcmRlci1yYWRpdXM6IHZhcigtLWJvcmRlci1yYWRpdXMpOwogICAgYm9yZGVyLXNwYWNpbmc6IDA7CiAgICBtYXgtd2lkdGg6IDEwMCU7CiAgICBvdmVyZmxvdzogaGlkZGVuOwogICAgcGFkZGluZzogMDsKfQoKdGFibGUgdGQsCnRhYmxlIHRoLAp0YWJsZSB0ciB7CiAgICBwYWRkaW5nOiAwLjRyZW0gMC44cmVtOwogICAgdGV4dC1hbGlnbjogdmFyKC0tanVzdGlmeS1pbXBvcnRhbnQpOwp9Cgp0YWJsZSB0aGVhZCB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvcik7CiAgICBib3JkZXItY29sbGFwc2U6IGNvbGxhcHNlOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBjb2xvcjogdmFyKC0tY29sb3ItYmcpOwogICAgbWFyZ2luOiAwOwogICAgcGFkZGluZzogMDsKfQoKdGFibGUgdGhlYWQgdGg6Zmlyc3QtY2hpbGQgewogICAgYm9yZGVyLXRvcC1sZWZ0LXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7Cn0KCnRhYmxlIHRoZWFkIHRoOmxhc3QtY2hpbGQgewogICAgYm9yZGVyLXRvcC1yaWdodC1yYWRpdXM6IHZhcigtLWJvcmRlci1yYWRpdXMpOwp9Cgp0YWJsZSB0aGVhZCB0aDpmaXJzdC1jaGlsZCwKdGFibGUgdHIgdGQ6Zmlyc3QtY2hpbGQgewogICAgdGV4dC1hbGlnbjogdmFyKC0tanVzdGlmeS1ub3JtYWwpOwp9CgovKiBRdW90ZXMgKi8KYmxvY2txdW90ZSB7CiAgICBkaXNwbGF5OiBibG9jazsKICAgIGZvbnQtc2l6ZTogeC1sYXJnZTsKICAgIGxpbmUtaGVpZ2h0OiB2YXIoLS1saW5lLWhlaWdodCk7CiAgICBtYXJnaW46IDFyZW0gYXV0bzsKICAgIG1heC13aWR0aDogdmFyKC0td2lkdGgtY2FyZC1tZWRpdW0pOwogICAgcGFkZGluZzogMS41cmVtIDFyZW07CiAgICB0ZXh0LWFsaWduOiB2YXIoLS1qdXN0aWZ5LWltcG9ydGFudCk7Cn0KCmJsb2NrcXVvdGUgZm9vdGVyIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci10ZXh0LXNlY29uZGFyeSk7CiAgICBkaXNwbGF5OiBibG9jazsKICAgIGZvbnQtc2l6ZTogc21hbGw7CiAgICBsaW5lLWhlaWdodDogdmFyKC0tbGluZS1oZWlnaHQpOwogICAgcGFkZGluZzogMS41cmVtIDA7Cn0KCi8qIEN1c3RvbSBzdHlsZXMgKi8K")

	tmpls["html/_footer.html"] = tostring("PGRpdiBjbGFzcz1jb250YWluZXI+CiAgPGZvb3RlciBjbGFzcz0icHQtNCBteS1tZC01IHB0LW1kLTUgYm9yZGVyLXRvcCI+CiAgICA8ZGl2IGNsYXNzPSJyb3ciPgogICAgICA8ZGl2IGNsYXNzPSJjb2wtNiBjb2wtbWQtMyBvZmZzZXQtbWQtMyB0ZXh0LW1kLXJpZ2h0Ij4KICAgICAgICA8aDU+TmF2aWdhdGlvbjwvaDU+CiAgICAgICAgPHVsIGNsYXNzPSJsaXN0LXVuc3R5bGVkIHRleHQtc21hbGwiPgogICAgICAgICAge3sgaWYgLlNwYWNlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8nPkhvbWU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnRUeXBlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Ib29rIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Db250ZW50IH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9jb250ZW50dHlwZS97eyAuU3BhY2UuSUR9fS97eyAuQ29udGVudFR5cGUuSUQgfX0nPnt7IC5Db250ZW50VHlwZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIGFuZCAuU3BhY2UgKG5vdCAuQ29udGVudFR5cGUpIChub3QgLkhvb2spIH19CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjY29weU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+Q29weTwvYT48L2xpPgogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgYW5kIC5Db250ZW50VHlwZSAobm90IC5Db250ZW50KSB9fQogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnQgfX0KICAgICAgICAgICAgPGxpPjxpbnB1dCB0eXBlPXN1Ym1pdCBjbGFzcz0idGV4dC1kZWNvcmF0aW9uLW5vbmUgbS0wIHAtMCBidG4gYnRuLWxpbmsgdGV4dC1tdXRlZCBib3JkZXItMCIgdmFsdWU9U2F2ZSAvPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkhvb2sgfX0KICAgICAgICAgICAgPGxpPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiNkZWxldGVNb2RhbCIgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9JyMnPkRlbGV0ZTwvYT48L2xpPgogICAgICAgICAge3sgZW5kIH19CiAgICAgICAgICB7eyBpZiAuVXNlciB9fQogICAgICAgICAgICA8bGk+CiAgICAgICAgICAgICAgPGZvcm0gbWV0aG9kPVBPU1QgYWN0aW9uPScvdXNlci9sb2dvdXQnIGVuY3R5cGU9J211bHRpcGFydC9mb3JtLWRhdGEnPgogICAgICAgICAgICAgICAgPGlucHV0IHR5cGU9c3VibWl0IGNsYXNzPSJ0ZXh0LWRlY29yYXRpb24tbm9uZSBtLTAgcC0wIGJ0biBidG4tbGluayB0ZXh0LW11dGVkIGJvcmRlci0wIiB2YWx1ZT1Mb2dvdXQgLz4KICAgICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICAgIDwvbGk+CiAgICAgICAgICAgIHt7aWYgYW5kIC5Vc2VyICguVXNlciB8IHBhaWQpfX0KICAgICAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjaW52aXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5JbnZpdGU8L2E+PC9saT4KICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9wYWdlL2JpbGxpbmcnPkJpbGxpbmc8L2E+PC9saT4KICAgICAgICAgIHt7IGVsc2UgfX0KICAgICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvJz5Ib21lPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLyNzaWdudXAnPlNpZ251cDwvYT48L2xpPgogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8jbG9naW4nPkxvZ2luPC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmR9fQogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvL2dpdC5zci5odC9+ZXZhbmovY21zJz5Tb3VyY2U8L2E+PC9saT4KICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLy9naXQuc3IuaHQvfmV2YW5qL2Ntcy90cmVlL21hc3Rlci9MSUNFTlNFJz5MaWNlbnNlPC9hPjwvbGk+CiAgICAgICAgPC91bD4KICAgICAgPC9kaXY+CiAgICAgIDxkaXYgY2xhc3M9ImNvbC02IGNvbC1tZC0zIj4KICAgICAgICA8aDU+UmVzb3VyY2VzPC9oNT4KICAgICAgICA8dWwgY2xhc3M9Imxpc3QtdW5zdHlsZWQgdGV4dC1zbWFsbCI+CiAgICAgICAgICB7e2lmIC5Vc2VyfX0KICAgICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvcGFnZS9iaWxsaW5nJz5CaWxsaW5nPC9hPjwvbGk+CiAgICAgICAgICB7e2Vsc2V9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8jcHJpY2luZyc+UHJpY2luZzwvYT48L2xpPgogICAgICAgICAge3tlbmR9fQogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvcGFnZS90b3VyJz5Ub3VyPC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9wYWdlL2RvYyc+RG9jczwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvcGFnZS9mYXEiPkZBUTwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvcGFnZS90ZXJtcyI+VGVybXM8L2E+PC9saT4KICAgICAgICAgIDxsaT48YSBjbGFzcz0idGV4dC1tdXRlZCIgaHJlZj0iL3BhZ2UvcHJpdmFjeSI+UHJpdmFjeTwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvcGFnZS9jb250YWN0Ij5Db250YWN0PC9hPjwvbGk+CiAgICAgICAgPC91bD4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9cm93PgogICAgICA8cCBjbGFzcz0ndGV4dC1tdXRlZCB0ZXh0LWNlbnRlciBtdC01IHctMTAwIHRleHQtdHJ1bmNhdGUgb3ZlcmZsb3ctaGlkZGVuJz52Lnt7LkJ1aWxkfX08L3A+CiAgICA8L2Rpdj4KICA8L2Zvb3Rlcj4KPC9kaXY+Cgp7e2lmIGFuZCAuVXNlciAoLlVzZXIgfCBwYWlkKX19Cjxmb3JtIG1ldGhvZD1QT1NUIGFjdGlvbj0nL2ludml0ZScgZW5jdHlwZT0nbXVsdGlwYXJ0L2Zvcm0tZGF0YSc+CiAgPGlucHV0IHR5cGU9aGlkZGVuIG5hbWU9bWV0aG9kIHZhbHVlPVBPU1QgLz4KICA8aW5wdXQgdHlwZT1oaWRkZW4gbmFtZT1tZXRob2QgdmFsdWU9Int7LlVzZXIuT3JnLklEfX0iIC8+CiAgPGRpdiBjbGFzcz0ibW9kYWwgZmFkZSIgaWQ9Imludml0ZU1vZGFsIiB0YWJpbmRleD0iLTEiIHJvbGU9ImRpYWxvZyIgYXJpYS1sYWJlbGxlZGJ5PSJleGFtcGxlTW9kYWxMYWJlbCIgYXJpYS1oaWRkZW49InRydWUiPgogICAgPGRpdiBjbGFzcz0ibW9kYWwtZGlhbG9nIG1vZGFsLWRpYWxvZy1zY3JvbGxhYmxlIj4KICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtY29udGVudCI+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtaGVhZGVyIj4KICAgICAgICAgIDxoNSBjbGFzcz0ibW9kYWwtdGl0bGUiIGlkPSJleGFtcGxlTW9kYWxMYWJlbCI+SW52aXRlIHNvbWVvbmUgdG8geW91ciBzcGFjZShzKTwvaDU+CiAgICAgICAgICA8YnV0dG9uIHR5cGU9ImJ1dHRvbiIgY2xhc3M9ImNsb3NlIiBkYXRhLWRpc21pc3M9Im1vZGFsIiBhcmlhLWxhYmVsPSJDbG9zZSI+CiAgICAgICAgICAgIDxzcGFuIGFyaWEtaGlkZGVuPSJ0cnVlIj4mdGltZXM7PC9zcGFuPgogICAgICAgICAgPC9idXR0b24+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtYm9keSI+CiAgICAgICAgICA8cD5XZSdsbCBnZW5lcmF0ZSBhIHNwZWNpYWwgbGluayBmb3IgeW91LiBTZW5kIHRoaXMgdG8geW91ciBmcmllbmQsIAogICAgICAgICAgY293b3JrZXIsIG9yIHdob2V2ZXIhPC9wPgogICAgICAgICAgPHA+VGhlIGludml0ZSB3aWxsIG9ubHkgYmUgYWN0aXZlIGZvciBvbmUgaG91ci48L3A+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtZm9vdGVyIj4KICAgICAgICAgIDxidXR0b24gdHlwZT0iYnV0dG9uIiBjbGFzcz0iYnRuIGJ0bi1zZWNvbmRhcnkiIGRhdGEtZGlzbWlzcz0ibW9kYWwiPkNsb3NlPC9idXR0b24+CiAgICAgICAgICA8YnV0dG9uIHR5cGU9InN1Ym1pdCIgY2xhc3M9ImJ0biBidG4tcHJpbWFyeSI+R288L2J1dHRvbj4KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICA8L2Rpdj4KPC9mb3JtPgp7e2VuZH19Cgp7e2lmIC5BfX0KPGltZyBzdHlsZT0ncG9zaXRpb246IGZpeGVkOyBib3R0b206IDA7IHJpZ2h0OiAwOycgc3JjPSIvL3NraXBwZXJjbXMuZ29hdGNvdW50ZXIuY29tL2NvdW50P3A9e3suQS5QYXRofX17e2lmIC5BLlJlZmVycmVyfX0mcj17ey5BLlJlZmVycmVyfX17e2VuZH19JnJuZD17ey5BLlJORH19Ij4Ke3tlbmR9fQo=")
	tmpls["html/_footer.html"] = tostring("PGRpdiBjbGFzcz1jb250YWluZXI+CiAgPGZvb3RlciBjbGFzcz0icHQtNCBteS1tZC01IHB0LW1kLTUgYm9yZGVyLXRvcCI+CiAgICA8ZGl2IGNsYXNzPSJyb3ciPgogICAgICA8ZGl2IGNsYXNzPSJjb2wtNiBjb2wtbWQtMyBvZmZzZXQtbWQtMyB0ZXh0LW1kLXJpZ2h0Ij4KICAgICAgICA8aDU+TmF2aWdhdGlvbjwvaDU+CiAgICAgICAgPHVsIGNsYXNzPSJsaXN0LXVuc3R5bGVkIHRleHQtc21hbGwiPgogICAgICAgICAge3sgaWYgLlNwYWNlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8nPkhvbWU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnRUeXBlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Ib29rIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Db250ZW50IH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9jb250ZW50dHlwZS97eyAuU3BhY2UuSUR9fS97eyAuQ29udGVudFR5cGUuSUQgfX0nPnt7IC5Db250ZW50VHlwZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIGFuZCAuU3BhY2UgKG5vdCAuQ29udGVudFR5cGUpIChub3QgLkhvb2spIH19CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjY29weU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+Q29weTwvYT48L2xpPgogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgYW5kIC5Db250ZW50VHlwZSAobm90IC5Db250ZW50KSB9fQogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnQgfX0KICAgICAgICAgICAgPGxpPjxpbnB1dCB0eXBlPXN1Ym1pdCBjbGFzcz0idGV4dC1kZWNvcmF0aW9uLW5vbmUgbS0wIHAtMCBidG4gYnRuLWxpbmsgdGV4dC1tdXRlZCBib3JkZXItMCIgdmFsdWU9U2F2ZSAvPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkhvb2sgfX0KICAgICAgICAgICAgPGxpPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiNkZWxldGVNb2RhbCIgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9JyMnPkRlbGV0ZTwvYT48L2xpPgogICAgICAgICAge3sgZW5kIH19CiAgICAgICAgICB7eyBpZiAuVXNlciB9fQogICAgICAgICAgICA8bGk+CiAgICAgICAgICAgICAgPGZvcm0gbWV0aG9kPVBPU1QgYWN0aW9uPScvdXNlci9sb2dvdXQnIGVuY3R5cGU9J211bHRpcGFydC9mb3JtLWRhdGEnPgogICAgICAgICAgICAgICAgPGlucHV0IHR5cGU9c3VibWl0IGNsYXNzPSJ0ZXh0LWRlY29yYXRpb24tbm9uZSBtLTAgcC0wIGJ0biBidG4tbGluayB0ZXh0LW11dGVkIGJvcmRlci0wIiB2YWx1ZT1Mb2dvdXQgLz4KICAgICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICAgIDwvbGk+CiAgICAgICAgICAgIHt7aWYgYW5kIC5Vc2VyICguVXNlciB8IHBhaWQpfX0KICAgICAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjaW52aXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5JbnZpdGU8L2E+PC9saT4KICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9wYWdlL2JpbGxpbmcnPkJpbGxpbmc8L2E+PC9saT4KICAgICAgICAgIHt7IGVsc2UgfX0KICAgICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvJz5Ib21lPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLyNzaWdudXAnPlNpZ251cDwvYT48L2xpPgogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8jbG9naW4nPkxvZ2luPC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmR9fQogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvL2dpdC5zci5odC9+ZXZhbmovY21zJz5Tb3VyY2U8L2E+PC9saT4KICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLy9naXQuc3IuaHQvfmV2YW5qL2Ntcy90cmVlL21hc3Rlci9MSUNFTlNFJz5MaWNlbnNlPC9hPjwvbGk+CiAgICAgICAgPC91bD4KICAgICAgPC9kaXY+CiAgICAgIDxkaXYgY2xhc3M9ImNvbC02IGNvbC1tZC0zIj4KICAgICAgICA8aDU+UmVzb3VyY2VzPC9oNT4KICAgICAgICA8dWwgY2xhc3M9Imxpc3QtdW5zdHlsZWQgdGV4dC1zbWFsbCI+CiAgICAgICAgICB7e2lmIC5Vc2VyfX0KICAgICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvcGFnZS9iaWxsaW5nJz5CaWxsaW5nPC9hPjwvbGk+CiAgICAgICAgICB7e2Vsc2V9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8jcHJpY2luZyc+UHJpY2luZzwvYT48L2xpPgogICAgICAgICAge3tlbmR9fQogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvcGFnZS90b3VyJz5Ub3VyPC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9wYWdlL2RvYyc+RG9jczwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvcGFnZS9mYXEiPkZBUTwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvcGFnZS90ZXJtcyI+VGVybXM8L2E+PC9saT4KICAgICAgICAgIDxsaT48YSBjbGFzcz0idGV4dC1tdXRlZCIgaHJlZj0iL3BhZ2UvcHJpdmFjeSI+UHJpdmFjeTwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvcGFnZS9jb250YWN0Ij5Db250YWN0PC9hPjwvbGk+CiAgICAgICAgPC91bD4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9cm93PgogICAgICA8cCBjbGFzcz0ndGV4dC1tdXRlZCB0ZXh0LWNlbnRlciBtdC01IHctMTAwIHRleHQtdHJ1bmNhdGUgb3ZlcmZsb3ctaGlkZGVuJz52Lnt7LkJ1aWxkfX08L3A+CiAgICA8L2Rpdj4KICA8L2Zvb3Rlcj4KPC9kaXY+Cgp7e2lmIGFuZCAuVXNlciAoLlVzZXIgfCBwYWlkKX19Cjxmb3JtIG1ldGhvZD1QT1NUIGFjdGlvbj0nL2ludml0ZScgZW5jdHlwZT0nbXVsdGlwYXJ0L2Zvcm0tZGF0YSc+CiAgPGlucHV0IHR5cGU9aGlkZGVuIG5hbWU9bWV0aG9kIHZhbHVlPVBPU1QgLz4KICA8aW5wdXQgdHlwZT1oaWRkZW4gbmFtZT1tZXRob2QgdmFsdWU9Int7LlVzZXIuT3JnLklEfX0iIC8+CiAgPGRpdiBjbGFzcz0ibW9kYWwgZmFkZSIgaWQ9Imludml0ZU1vZGFsIiB0YWJpbmRleD0iLTEiIHJvbGU9ImRpYWxvZyIgYXJpYS1sYWJlbGxlZGJ5PSJleGFtcGxlTW9kYWxMYWJlbCIgYXJpYS1oaWRkZW49InRydWUiPgogICAgPGRpdiBjbGFzcz0ibW9kYWwtZGlhbG9nIG1vZGFsLWRpYWxvZy1zY3JvbGxhYmxlIj4KICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtY29udGVudCI+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtaGVhZGVyIj4KICAgICAgICAgIDxoNSBjbGFzcz0ibW9kYWwtdGl0bGUiIGlkPSJleGFtcGxlTW9kYWxMYWJlbCI+SW52aXRlIHNvbWVvbmUgdG8geW91ciBzcGFjZShzKTwvaDU+CiAgICAgICAgICA8YnV0dG9uIHR5cGU9ImJ1dHRvbiIgY2xhc3M9ImNsb3NlIiBkYXRhLWRpc21pc3M9Im1vZGFsIiBhcmlhLWxhYmVsPSJDbG9zZSI+CiAgICAgICAgICAgIDxzcGFuIGFyaWEtaGlkZGVuPSJ0cnVlIj4mdGltZXM7PC9zcGFuPgogICAgICAgICAgPC9idXR0b24+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtYm9keSI+CiAgICAgICAgICA8cD5XZSdsbCBnZW5lcmF0ZSBhIHNwZWNpYWwgbGluayBmb3IgeW91LiBTZW5kIHRoaXMgdG8geW91ciBmcmllbmQsIAogICAgICAgICAgY293b3JrZXIsIG9yIHdob2V2ZXIhPC9wPgogICAgICAgICAgPHA+VGhlIGludml0ZSB3aWxsIG9ubHkgYmUgYWN0aXZlIGZvciBvbmUgaG91ci48L3A+CiAgICAgICAgICA8ZGl2IGNsYXNzPSdmb3JtLWdyb3VwIHJvdyc+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9J2NvbC0xMic+CiAgICAgICAgICAgICAgPGxhYmVsIGZvcj1yb2xlPlVzZXIncyBkZXNpcmVkIHJvbGU8L2xhYmVsPgogICAgICAgICAgICAgIDxzZWxlY3QgaWQ9cm9sZSBjbGFzcz0idy0xMDAgZm9ybS1jb250cm9sIiBuYW1lPXJvbGUgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgICA8b3B0aW9uIGRpc2FibGVkIHZhbHVlPlJvbGU8L29wdGlvbj4KICAgICAgICAgICAgICAgIHt7cmFuZ2UgLlJvbGVzfX0KICAgICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9Int7Lk5hbWV9fSI+e3suTmFtZX19PC9vcHRpb24+CiAgICAgICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICAgICAgPC9zZWxlY3Q+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtZm9vdGVyIj4KICAgICAgICAgIDxidXR0b24gdHlwZT0iYnV0dG9uIiBjbGFzcz0iYnRuIGJ0bi1zZWNvbmRhcnkiIGRhdGEtZGlzbWlzcz0ibW9kYWwiPkNsb3NlPC9idXR0b24+CiAgICAgICAgICA8YnV0dG9uIHR5cGU9InN1Ym1pdCIgY2xhc3M9ImJ0biBidG4tcHJpbWFyeSI+R288L2J1dHRvbj4KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICA8L2Rpdj4KPC9mb3JtPgp7e2VuZH19Cgp7e2lmIC5BfX0KPGltZyBzdHlsZT0ncG9zaXRpb246IGZpeGVkOyBib3R0b206IDA7IHJpZ2h0OiAwOycgc3JjPSIvL3NraXBwZXJjbXMuZ29hdGNvdW50ZXIuY29tL2NvdW50P3A9e3suQS5QYXRofX17e2lmIC5BLlJlZmVycmVyfX0mcj17ey5BLlJlZmVycmVyfX17e2VuZH19JnJuZD17ey5BLlJORH19Ij4Ke3tlbmR9fQo=")

	tmpls["html/_head.html"] = tostring("PG1ldGEgY2hhcnNldD0ndXRmLTgnPgo8bWV0YSBuYW1lPSd2aWV3cG9ydCcgY29udGVudD0nd2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEnPgo8bGluayByZWw9J2ljb24nIHR5cGU9J2ltYWdlL3gtaWNvbicgaHJlZj0naHR0cHM6Ly9mYXZpY29uLmV2YW5qb24uZXMvMC8xMDUvMjE3LzMyL2Zhdmljb24uaWNvJyAvPgo8bGluayByZWw9J3N0eWxlc2hlZXQnIGhyZWY9Jy9zdGF0aWMvY3NzL2Jvb3RzdHJhcC5taW4uY3NzJyAvPgo=")